diff --git a/.gitignore b/.gitignore index 52945c239..456b9cf1f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ npm-debug.log dist node_modules lerna-debug.log +*.swp coverage/ diff --git a/packages/optimizely-sdk/lib/index.node.tests.js b/packages/optimizely-sdk/lib/index.node.tests.js index 5fdf26457..bb7750d90 100644 --- a/packages/optimizely-sdk/lib/index.node.tests.js +++ b/packages/optimizely-sdk/lib/index.node.tests.js @@ -28,37 +28,58 @@ describe('optimizelyFactory', function() { describe('createInstance', function() { var fakeErrorHandler = { handleError: function() {}}; var fakeEventDispatcher = { dispatchEvent: function() {}}; - var fakeLogger = { log: function() {}}; + var fakeLogger; beforeEach(function() { - sinon.spy(console, 'error'); sinon.stub(configValidator, 'validate'); + + fakeLogger = { log: sinon.spy() }; + sinon.stub(logger, 'createLogger').returns(fakeLogger); }); afterEach(function() { - console.error.restore(); + logger.createLogger.restore(); configValidator.validate.restore(); }); - it('should not throw if the provided config is not valid and call console.error if logger is passed in', function() { - configValidator.validate.throws(new Error('Invalid config or something')); - assert.doesNotThrow(function() { - optimizelyFactory.createInstance({ - datafile: {}, - logger: logger.createLogger({ logLevel: enums.LOG_LEVEL.INFO }), + context('config fails validation', function() { + beforeEach(function() { + configValidator.validate.throws(new Error('Invalid config or something')); + }); + + it('catches internal errors', function() { + assert.doesNotThrow(function() { + optimizelyFactory.createInstance({}); }); }); - assert.isTrue(console.error.called); - }); - it('should not throw if the provided config is not valid and call console.error if no-op logger is used', function() { - configValidator.validate.throws(new Error('Invalid config or something')); - assert.doesNotThrow(function() { - optimizelyFactory.createInstance({ - datafile: {}, + context('a logger was supplied', function() { + it('an error message is submitted to the supplied logger', function() { + var customLogger = { log: sinon.spy() }; + + optimizelyFactory.createInstance({ + datafile: {}, + logger: customLogger, + }); + + sinon.assert.notCalled(fakeLogger.log); + // Once for config failing validation, and again within the `Optimizely` + // constructor for not supplying a valid datafile. + sinon.assert.calledTwice(customLogger.log); + sinon.assert.alwaysCalledWith(customLogger.log, enums.LOG_LEVEL.ERROR); + }); + }); + + context('a logger was not supplied', function() { + it('an error message is submitted to a simple logger', function() { + optimizelyFactory.createInstance({}); + + // Only the validation error is logged. + // The `Optimizely` constructor error is submitted to a NoOpLogger. + sinon.assert.calledOnce(fakeLogger.log); + sinon.assert.calledWith(fakeLogger.log, enums.LOG_LEVEL.ERROR); }); }); - assert.isTrue(console.error.called); }); it('should create an instance of optimizely', function() { diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index c9c167a92..e087a09dd 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -55,200 +55,198 @@ describe('lib/optimizely', function() { createdLogger.log.restore(); }); - describe('constructor', function() { - it('should construct an instance of the Optimizely class', function() { - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - assert.instanceOf(optlyInstance, Optimizely); - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); + it('should construct an instance of the Optimizely class', function() { + var optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestProjectConfig(), + errorHandler: stubErrorHandler, + eventDispatcher: stubEventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, }); + assert.instanceOf(optlyInstance, Optimizely); + sinon.assert.called(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); + }); - it('should construct an instance of the Optimizely class when datafile is JSON string', function() { - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: JSON.stringify(testData.getTestProjectConfig()), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - assert.instanceOf(optlyInstance, Optimizely); - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); + it('should construct an instance of the Optimizely class when datafile is JSON string', function() { + var optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: JSON.stringify(testData.getTestProjectConfig()), + errorHandler: stubErrorHandler, + eventDispatcher: stubEventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, }); + assert.instanceOf(optlyInstance, Optimizely); + sinon.assert.called(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); + }); - it('should log if the client engine passed in is invalid', function() { - new Optimizely({ - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - logger: createdLogger, - }); + it('should log if the client engine passed in is invalid', function() { + new Optimizely({ + datafile: testData.getTestProjectConfig(), + errorHandler: stubErrorHandler, + eventDispatcher: stubEventDispatcher, + logger: createdLogger, + }); - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, 'OPTIMIZELY', 'undefined')); + sinon.assert.called(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, 'OPTIMIZELY', 'undefined')); + }); + + it('should throw an error if a datafile is not passed into the constructor', function() { + var optly = new Optimizely({ + clientEngine: 'node-sdk', + errorHandler: stubErrorHandler, + logger: createdLogger, }); + sinon.assert.calledOnce(stubErrorHandler.handleError); + var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; + assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'OPTIMIZELY')); - it('should throw an error if a datafile is not passed into the constructor', function() { - var optly = new Optimizely({ - clientEngine: 'node-sdk', - errorHandler: stubErrorHandler, - logger: createdLogger, - }); - sinon.assert.calledOnce(stubErrorHandler.handleError); - var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'OPTIMIZELY')); + sinon.assert.calledOnce(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'OPTIMIZELY')); - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'OPTIMIZELY')); + assert.isFalse(optly.isValidInstance); + }); + + it('should throw an error if the datafile JSON is malformed', function() { + var invalidDatafileJSON = 'abc'; + + new Optimizely({ + clientEngine: 'node-sdk', + errorHandler: stubErrorHandler, + datafile: invalidDatafileJSON, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + }); + + sinon.assert.calledOnce(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, 'OPTIMIZELY')); + }); + + it('should throw an error if the datafile is not valid', function() { + var invalidDatafile = testData.getTestProjectConfig(); + delete invalidDatafile['projectId']; + + new Optimizely({ + clientEngine: 'node-sdk', + errorHandler: stubErrorHandler, + datafile: invalidDatafile, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + }); + sinon.assert.calledOnce(stubErrorHandler.handleError); + var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; + assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); + + sinon.assert.calledOnce(createdLogger.log); + var logMessage = createdLogger.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); + }); - assert.isFalse(optly.isValidInstance); + describe('skipping JSON schema validation', function() { + beforeEach(function() { + sinon.spy(jsonSchemaValidator, 'validate'); }); - it('should throw an error if the datafile JSON is malformed', function() { - var invalidDatafileJSON = 'abc'; + afterEach(function() { + jsonSchemaValidator.validate.restore(); + }); + it('should skip JSON schema validation if skipJSONValidation is passed into instance args with `true` value', function() { new Optimizely({ clientEngine: 'node-sdk', + datafile: testData.getTestProjectConfig(), errorHandler: stubErrorHandler, - datafile: invalidDatafileJSON, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, + eventDispatcher: stubEventDispatcher, + logger: logger.createLogger(), + skipJSONValidation: true, }); - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, 'OPTIMIZELY')); + sinon.assert.notCalled(jsonSchemaValidator.validate); }); - it('should throw an error if the datafile is not valid', function() { - var invalidDatafile = testData.getTestProjectConfig(); - delete invalidDatafile['projectId']; - + it('should not skip JSON schema validation if skipJSONValidation is passed into instance args with any value other than true', function() { new Optimizely({ clientEngine: 'node-sdk', + datafile: testData.getTestProjectConfig(), errorHandler: stubErrorHandler, - datafile: invalidDatafile, + eventDispatcher: stubEventDispatcher, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, + skipJSONValidation: 'hi', }); - sinon.assert.calledOnce(stubErrorHandler.handleError); - var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); + sinon.assert.calledOnce(jsonSchemaValidator.validate); sinon.assert.calledOnce(createdLogger.log); var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); + assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); }); + }); - describe('skipping JSON schema validation', function() { - beforeEach(function() { - sinon.spy(jsonSchemaValidator, 'validate'); - }); + describe('when a user profile service is provided', function() { + beforeEach(function() { + sinon.stub(decisionService, 'createDecisionService'); + }); - afterEach(function() { - jsonSchemaValidator.validate.restore(); - }); + afterEach(function() { + decisionService.createDecisionService.restore(); + }); - it('should skip JSON schema validation if skipJSONValidation is passed into instance args with `true` value', function() { - new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - logger: logger.createLogger(), - skipJSONValidation: true, - }); + it('should validate and pass the user profile service to the decision service', function() { + var userProfileServiceInstance = { + lookup: function() {}, + save: function() {}, + }; - sinon.assert.notCalled(jsonSchemaValidator.validate); + var optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + logger: createdLogger, + datafile: testData.getTestProjectConfig(), + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: userProfileServiceInstance, }); - it('should not skip JSON schema validation if skipJSONValidation is passed into instance args with any value other than true', function() { - new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - skipJSONValidation: 'hi', - }); - - sinon.assert.calledOnce(jsonSchemaValidator.validate); - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); + sinon.assert.calledWith(decisionService.createDecisionService, { + configObj: optlyInstance.configObj, + userProfileService: userProfileServiceInstance, + logger: createdLogger, }); + + // Checking the second log message as the first one just says "Datafile is valid" + var logMessage = createdLogger.log.args[1][1]; + assert.strictEqual(logMessage, 'OPTIMIZELY: Valid user profile service provided.'); }); - describe('when a user profile service is provided', function() { - beforeEach(function() { - sinon.stub(decisionService, 'createDecisionService'); - }); + it('should pass in a null user profile to the decision service if the provided user profile is invalid', function() { + var invalidUserProfile = { + save: function() {}, + }; - afterEach(function() { - decisionService.createDecisionService.restore(); + var optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + logger: createdLogger, + datafile: testData.getTestProjectConfig(), + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: invalidUserProfile, }); - it('should validate and pass the user profile service to the decision service', function() { - var userProfileServiceInstance = { - lookup: function() {}, - save: function() {}, - }; - - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - logger: createdLogger, - datafile: testData.getTestProjectConfig(), - jsonSchemaValidator: jsonSchemaValidator, - userProfileService: userProfileServiceInstance, - }); - - sinon.assert.calledWith(decisionService.createDecisionService, { - configObj: optlyInstance.configObj, - userProfileService: userProfileServiceInstance, - logger: createdLogger, - }); - - // Checking the second log message as the first one just says "Datafile is valid" - var logMessage = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage, 'OPTIMIZELY: Valid user profile service provided.'); + sinon.assert.calledWith(decisionService.createDecisionService, { + configObj: optlyInstance.configObj, + userProfileService: null, + logger: createdLogger, }); - it('should pass in a null user profile to the decision service if the provided user profile is invalid', function() { - var invalidUserProfile = { - save: function() {}, - }; - - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - logger: createdLogger, - datafile: testData.getTestProjectConfig(), - jsonSchemaValidator: jsonSchemaValidator, - userProfileService: invalidUserProfile, - }); - - sinon.assert.calledWith(decisionService.createDecisionService, { - configObj: optlyInstance.configObj, - userProfileService: null, - logger: createdLogger, - }); - - // Checking the second log message as the first one just says "Datafile is valid" - var logMessage = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage, 'USER_PROFILE_SERVICE_VALIDATOR: Provided user profile service instance is in an invalid format: Missing function \'lookup\'.'); - }); + // Checking the second log message as the first one just says "Datafile is valid" + var logMessage = createdLogger.log.args[1][1]; + assert.strictEqual(logMessage, 'USER_PROFILE_SERVICE_VALIDATOR: Provided user profile service instance is in an invalid format: Missing function \'lookup\'.'); }); }); }); diff --git a/packages/optimizely-sdk/lib/plugins/logger/index.js b/packages/optimizely-sdk/lib/plugins/logger/index.js index cab968c24..fd1de7f5d 100644 --- a/packages/optimizely-sdk/lib/plugins/logger/index.js +++ b/packages/optimizely-sdk/lib/plugins/logger/index.js @@ -78,6 +78,15 @@ Logger.prototype.setLogLevel = function(logLevel) { * @private */ Logger.prototype.__shouldLog = function(targetLogLevel) { + try { + if (process.env.NODE_ENV === 'test') { + // Suppress logs during testing. + return false; + } + } catch (e) { + // `process` is likely a ReferenceError in non-Node.js environments + } + return targetLogLevel >= this.logLevel; }; diff --git a/packages/optimizely-sdk/lib/plugins/logger/index.tests.js b/packages/optimizely-sdk/lib/plugins/logger/index.tests.js index 808027bd8..c71a8e106 100644 --- a/packages/optimizely-sdk/lib/plugins/logger/index.tests.js +++ b/packages/optimizely-sdk/lib/plugins/logger/index.tests.js @@ -33,9 +33,20 @@ describe('lib/plugins/logger', function() { }); describe('log', function() { + var nodeEnv; + beforeEach(function() { defaultLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); sinon.stub(defaultLogger, '__consoleLog'); + + // The usual rules of turning off logging don't apply here, + // since the code under test _is the logger_. + nodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + }); + + afterEach(function() { + process.env.NODE_ENV = nodeEnv; }); it('should log the given message', function() { diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index b99793de9..98f44842c 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -5,7 +5,7 @@ "main": "lib/index.node.js", "browser": "lib/index.browser.js", "scripts": { - "test": "mocha ./lib/*.tests.js ./lib/**/*.tests.js ./lib/**/**/*tests.js --recursive", + "test": "NODE_ENV=test mocha --reporter dot './lib/**/*.tests.js'", "test-xbrowser": "karma start karma.bs.conf.js --single-run", "lint": "eslint lib/**", "cover": "istanbul cover _mocha ./lib/*.tests.js ./lib/**/*.tests.js ./lib/**/**/*tests.js",