UNPKG

quodolores

Version:

Monorepo for the Firebase JavaScript SDK

1,311 lines (1,239 loc) 84.6 kB
/** * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Tests for recaptchaverifier.js. */ goog.provide('fireauth.RecaptchaVerifierTest'); goog.require('fireauth.AuthError'); goog.require('fireauth.BaseRecaptchaVerifier'); goog.require('fireauth.GRecaptchaMockFactory'); goog.require('fireauth.RecaptchaRealLoader'); goog.require('fireauth.RecaptchaVerifier'); goog.require('fireauth.RpcHandler'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.common.testHelper'); goog.require('fireauth.constants'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.Uri'); goog.require('goog.dom'); goog.require('goog.html.TrustedResourceUrl'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.MockControl'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.TestCase'); goog.require('goog.testing.events'); goog.require('goog.testing.jsunit'); goog.require('goog.testing.mockmatchers'); goog.require('goog.testing.recordFunction'); goog.setTestOnly('fireauth.RecaptchaVerifierTest'); var mockControl; var stubs = new goog.testing.PropertyReplacer(); var app; var grecaptcha; var myElement, myElement2; var ignoreArgument; var loaderInstance; var clock; var grecaptchaMock; var randomCounter; var startInstanceId = fireauth.GRecaptchaMockFactory.START_INSTANCE_ID; var expirationTimeMs = fireauth.GRecaptchaMockFactory.EXPIRATION_TIME_MS; var solveTimeMs = fireauth.GRecaptchaMockFactory.SOLVE_TIME_MS; /** * Initialize reCAPTCHA mocks. This mocks the grecaptcha library. */ function initializeRecaptchaMocks() { var recaptcha = { // Recaptcha challenge ID. 'challengesId': 0, // Recaptcha array of instances. 'instances': [], // Render reCAPTCHA instance. 'render': function(container, parameters) { // New widget ID. var id = recaptcha.instances.length; // Element container. var ele = goog.dom.getElement(container); // Store new reCAPTCHA instance and its parameters. recaptcha.instances.push({ 'container': container, 'response': 'response-' + recaptcha.challengesId, 'userResponse': '', 'parameters': parameters, 'callback': parameters['callback'] || null, 'expired-callback': parameters['expired-callback'] || null, 'execute': false }); // Increment challenges ID. recaptcha.challengesId++; if (parameters.size !== 'invisible') { // recaptcha can only be rendered on an empty element. assertFalse(goog.dom.getChildren(ele).length > 0); // Fill container with some HTML to simulate rendered widget. ele.innerHTML = '<div id="recaptchaInstance' + id + '"></div>'; } // Return the reCAPTCHA widget ID. return id; }, // Reset reCAPTCHA instance 'reset': function(opt_id) { // If widget ID not provided, use last created one. var id = typeof opt_id !== 'undefined' ? opt_id : recaptcha.instances.length - 1; // Assert instance exists. assertNotNullNorUndefined(recaptcha.instances[id]); var parameters = recaptcha.instances[id]['parameters']; // Reset instance challenge and its other properties. recaptcha.instances[id] = { 'container': parameters['container'], 'response': 'response-' + recaptcha.challengesId, 'userResponse': '', 'parameters': parameters, 'callback': parameters['callback'], 'expired-callback': parameters['expired-callback'], 'execute': false }; // Increment challenges ID. recaptcha.challengesId++; }, // Returns reCAPTCHA instance's user response. 'getResponse': function(opt_id) { // If widget ID not provided, use last created one. var id = typeof opt_id !== 'undefined' ? opt_id : recaptcha.instances.length - 1; // Assert instance exists. assertNotNullNorUndefined(recaptcha.instances[id]); // Return user response. return recaptcha.instances[id]['userResponse'] || ''; }, // Executes the invisible reCAPTCHA. This will either force a reCAPTCHA // visible challenge or resolve immediately. For testing, the former // scenario is used. 'execute': function(opt_id) { // If widget id not provided, use last created one. var id = typeof opt_id !== 'undefined' ? opt_id : recaptcha.instances.length - 1; // Assert instance exists. assertNotNullNorUndefined(recaptcha.instances[id]); var instance = recaptcha.instances[id]; var parameters = instance['parameters']; // execute should not be called on a visible reCAPTCHA. if (parameters['size'] !== 'invisible') { throw new Error('execute called on visible reCAPTCHA!'); } // Mark execute flag as true. instance['execute'] = true; }, // For internal testing, simulates the reCAPTCHA corresponding to ID passed // is solved. 'solveResponse': function(opt_id) { // If widget ID not provided, use last created one. var id = typeof opt_id !== 'undefined' ? opt_id : recaptcha.instances.length - 1; // Assert instance exists. assertNotNullNorUndefined(recaptcha.instances[id]); var instance = recaptcha.instances[id]; var parameters = instance['parameters']; // Updated user response with the solve response. instance['userResponse'] = instance['response']; // execute must have been called on invisible reCAPTCHA. if (!instance['execute'] && parameters['size'] === 'invisible') { throw new Error('execute needs to be called before solving response!'); } // Trigger reCAPTCHA callback. if (instance['callback'] && typeof instance['callback'] == 'function') { instance['callback'](instance['response']); } // Update next challenge response. instance['response'] = 'response-' + recaptcha.challengesId; recaptcha.challengesId++; }, // For internal testing, simulates the reCAPTCHA token corresponding to ID // passed is expired. 'expireResponse': function(opt_id) { // If widget ID not provided, use last created one. var id = typeof opt_id !== 'undefined' ? opt_id : recaptcha.instances.length - 1; // Assert instance exists. assertNotNullNorUndefined(recaptcha.instances[id]); var instance = recaptcha.instances[id]; // Reset user response. instance['userResponse'] = ''; // Trigger expired callback. if (instance['expired-callback'] && typeof instance['expired-callback'] == 'function') { instance['expired-callback'](); } // Reset execute. instance['execute'] = false; } }; // Fake the Recaptcha global object. goog.global['grecaptcha'] = recaptcha; } /** * Asserts the expected parameters used to initialize the reCAPTCHA. * @param {number} widgetId The reCAPTCHA widget ID. * @param {!Element|string} expectedContainer The expected reCAPTCHA container * parameter. * @param {!Object} expectedParams The expected parameters used to initialize * the reCAPTCHA. */ function assertRecaptchaParams(widgetId, expectedContainer, expectedParams) { // Confirm all expected parameters passed to the specified reCAPTCHA. // This check excludes callbacks. var instance = grecaptcha.instances[widgetId]; var actualParameters = instance['parameters']; for (var key in expectedParams) { if (expectedParams.hasOwnProperty(key) && key != 'callback' && key != 'expired-callback') { assertEquals(expectedParams[key], actualParameters[key]); } } // Confirm the reCAPTCHA initialized on the expected container. if (expectedParams.size !== 'invisible') { // For visible reCAPTCHA, confirm expectedContainer element matches the // parent of the actual container. assertEquals( goog.dom.getElement(expectedContainer), goog.dom.getParentElement(instance.container)); } else { assertEquals(expectedContainer, instance.container); } } function setUp() { mockControl = new goog.testing.MockControl(); ignoreArgument = goog.testing.mockmatchers.ignoreArgument; mockControl.$resetAll(); app = null; // Create DIV test element and add to document. myElement = goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'recaptcha'}); document.body.appendChild(myElement); // Create another DIV test element and add to document. myElement2 = goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'recaptcha2'}); document.body.appendChild(myElement2); // Bypass singleton for tests so loaders are not shared among different tests. loaderInstance = new fireauth.RecaptchaRealLoader(); stubs.replace( fireauth.RecaptchaRealLoader, 'getInstance', function() { return loaderInstance; }); // Mock grecaptcha. var randomCounter = 0; var grecaptchaMock = new fireauth.GRecaptchaMockFactory(); stubs.replace( fireauth.GRecaptchaMockFactory, 'getInstance', function() { return grecaptchaMock; }); stubs.replace( fireauth.util, 'generateRandomAlphaNumericString', function(charCount) { assertEquals(50, charCount); return 'random' + (randomCounter++).toString(); }); } function tearDown() { // Destroy both elements. if (myElement) { goog.dom.removeNode(myElement); myElement = null; } if (myElement2) { goog.dom.removeNode(myElement2); myElement2 = null; } // Reset global grecaptcha. grecaptcha = null; delete grecaptcha; try { mockControl.$verifyAll(); } finally { mockControl.$tearDown(); } delete goog.global['devCallback']; delete goog.global['devExpiredCallback']; stubs.reset(); } /** * Sets the Auth service on the provided app instance if not already set. * @param {!firebase.app.App} app The Firebase app instance on which to * initialize the Auth service if not already available. */ function initializeAuthServiceOnApp(app) { // Do nothing if auth() already exists. if (typeof app.auth !== 'function') { // Use set as Auth doesn't exist on the App instance. stubs.set(app, 'auth', function() { if (!this.auth_) { this.auth_ = { settings: { // App verification enabled by default. appVerificationDisabledForTesting: false } }; } return this.auth_; }); } } /** * Simulates the current Auth language on the specified App instance. * @param {!firebase.app.App} app The expected Firebase App instance. * @param {?string} languageCode The default Auth language. */ function simulateAuthLanguage(app, languageCode) { initializeAuthServiceOnApp(app); app.auth().getLanguageCode = function() { return languageCode; }; } /** * Simulates the current Auth frameworks on the specified App instance. * @param {!firebase.app.App} app The expected Firebase app instance. * @param {!Array<string>} frameworks The current frameworks set on the Auth * instance. */ function simulateAuthFramework(app, frameworks) { initializeAuthServiceOnApp(app); app.auth().getFramework = function() { return frameworks; }; } /** * Install the test to run and runs it. * @param {string} id The test identifier. * @param {function():!goog.Promise} func The test function to run. * @return {!goog.Promise} The result of the test. */ function installAndRunTest(id, func) { /** * @return {?goog.Promise<void>} A promise that resolves on cleanup * completion. */ var cleanupAppAndClock = function() { var promises = []; for (var i = 0; i < firebase.apps.length; i++) { promises.push(firebase.apps[i].delete()); } var p = null; if (promises.length) { p = goog.Promise.all(promises).then(function() { // Dispose clock then. Disposing before will throw an error in IE 11. goog.dispose(clock); }); if (clock) { // Some IE browsers like IE 11, native promise hangs if this is not // called when clock is mocked. // app.delete() will hang (it uses the native Promise). clock.tick(); } } else if (clock) { goog.dispose(clock); } return p; }; var testCase = new goog.testing.TestCase(); testCase.addNewTest(id, func); var error = null; return testCase.runTestsReturningPromise().then(function(result) { assertTrue(result.complete); // Display error detected. if (result.errors.length) { fail(result.errors.join('\n')); } assertEquals(1, result.totalCount); assertEquals(1, result.runCount); assertEquals(1, result.successCount); assertEquals(0, result.errors.length); return cleanupAppAndClock(); }).thenCatch(function(err) { error = err; return cleanupAppAndClock(); }).then(function() { if (error) { throw error; } }); } function testBaseRecaptchaVerifier_noDOM() { return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { var isDOMSupported = mockControl.createMethodMock( fireauth.util, 'isDOMSupported'); isDOMSupported().$returns(false).$once(); mockControl.$replayAll(); var expectedError = new fireauth.AuthError( fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + 'environment with DOM support.'); var error = assertThrows(function() { new fireauth.BaseRecaptchaVerifier('API_KEY', 'id'); }); fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); } function testBaseRecaptchaVerifier_noHttpOrHttps() { return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { var isHttpOrHttps = mockControl.createMethodMock( fireauth.util, 'isHttpOrHttps'); isHttpOrHttps().$returns(false).$once(); mockControl.$replayAll(); var expectedError = new fireauth.AuthError( fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + 'environment.'); var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); return recaptchaVerifier.render().thenCatch(function(error) { fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); }); } function testBaseRecaptchaVerifier_worker() { return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { // This gets called in some underlying dependencies at various points. // It is not feasible counting the exact number of calls and the sequence // they get called. It is better to use property replacer to stub this // utility. stubs.replace( fireauth.util, 'isWorker', function() {return true;}); var isHttpOrHttps = mockControl.createMethodMock( fireauth.util, 'isHttpOrHttps'); isHttpOrHttps().$returns(true).$once(); mockControl.$replayAll(); var expectedError = new fireauth.AuthError( fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + 'environment.'); var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); return recaptchaVerifier.render().thenCatch(function(error) { fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); }); } function testBaseRecaptchaVerifier_withSitekey() { return installAndRunTest('testBaseAppVerifier_withSitekey', function() { var options = { sitekey: 'MY_SITE_KEY' }; var expectedError = new fireauth.AuthError( fireauth.authenum.Error.ARGUMENT_ERROR, 'sitekey should not be provided for reCAPTCHA as one is ' + 'automatically provisioned for the current project.'); var error = assertThrows(function() { new fireauth.BaseRecaptchaVerifier('API_KEY', myElement, options); }); fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); } function testBaseRecaptchaVerifier_visible_nonEmpty() { return installAndRunTest('testBaseAppVerifier_visible_nonEmpty', function() { var expectedError = new fireauth.AuthError( fireauth.authenum.Error.ARGUMENT_ERROR, 'reCAPTCHA container is either not found or already contains inner ' + 'elements!'); myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); var error = assertThrows(function() { new fireauth.BaseRecaptchaVerifier('API_KEY', myElement); }); fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); } function testBaseRecaptchaVerifier_invisible_nonEmpty() { return installAndRunTest( 'testBaseAppVerifier_invisible_nonEmpty', function() { myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); var returnValue = assertNotThrows(function() { return new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, {'size': 'invisible'}); }); assertTrue(returnValue instanceof fireauth.BaseRecaptchaVerifier); }); } function testBaseRecaptchaVerifier_invalidContainer() { return installAndRunTest('testBaseAppVerifier_invalidContainer', function() { var expectedError = new fireauth.AuthError( fireauth.authenum.Error.ARGUMENT_ERROR, 'reCAPTCHA container is either not found or already contains inner ' + 'elements!'); var error = assertThrows(function() { new fireauth.BaseRecaptchaVerifier('API_KEY', 'invalidId'); }); fireauth.common.testHelper.assertErrorEquals(expectedError, error); }); } function testBaseRecaptchaVerifier_validContainerId() { return installAndRunTest('testBaseAppVerifier_validContainerId', function() { app = firebase.initializeApp({ apiKey: 'API_KEY' }, 'test'); var returnValue = assertNotThrows(function() { return new fireauth.BaseRecaptchaVerifier( 'API_KEY', 'recaptcha', {'size': 'compact'}); }); assertTrue(returnValue instanceof fireauth.BaseRecaptchaVerifier); }); } function testBaseRecaptchaVerifier_render() { return installAndRunTest('testBaseAppVerifier_render', function() { // Confirm expected endpoint config and version passed to underlying RPC // handler. var version = '1.2.3'; var endpoint = fireauth.constants.Endpoint.STAGING; var endpointConfig = { 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, 'secureTokenEndpoint': endpoint.secureTokenEndpoint }; var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', endpointConfig, version) .$returns(rpcHandler); safeLoad(ignoreArgument) .$does(function(url) { var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); var callback = uri.getParameterValue('onload'); assertEquals('', uri.getParameterValue('hl')); initializeRecaptchaMocks(); goog.global[callback](); }) .$once(); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY', 'theme': 'light', 'type': 'image' }; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, undefined, function() {return null;}, version, endpointConfig); assertEquals('recaptcha', recaptchaVerifier['type']); // Confirm property is readonly. recaptchaVerifier['type'] = 'modified'; assertEquals('recaptcha', recaptchaVerifier['type']); return recaptchaVerifier.render().then(function(widgetId) { assertRecaptchaParams(widgetId, myElement, expectedParams); assertEquals(0, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); grecaptcha.solveResponse(0); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { // Same unexpired response returned. assertEquals('response-0', recaptchaToken); // Expire response. grecaptcha.expireResponse(0); var resp = recaptchaVerifier.verify(); // Solve response after expiration. New reCAPTCHA token should be // returned. grecaptcha.solveResponse(0); return resp; }).then(function(recaptchaToken) { assertEquals('response-1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_render_visible_testMode() { return installAndRunTest('testBaseAppVerifier_visible_testMode', function() { // Confirm expected endpoint config and version passed to underlying RPC // handler. var version = '1.2.3'; var responseCallback = goog.testing.recordFunction(); var expiredCallback = goog.testing.recordFunction(); // Record calls to grecaptchaMock.render. stubs.replace( fireauth.GRecaptchaMockFactory.prototype, 'render', goog.testing.recordFunction( fireauth.GRecaptchaMockFactory.prototype.render)); var endpoint = fireauth.constants.Endpoint.STAGING; var endpointConfig = { 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, 'secureTokenEndpoint': endpoint.secureTokenEndpoint }; var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', endpointConfig, version) .$returns(rpcHandler); safeLoad(ignoreArgument).$never(); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); // Install mock clock. clock = new goog.testing.MockClock(true); var params = { 'size': 'compact', 'theme': 'light', 'type': 'image', 'callback': responseCallback, 'expired-callback': expiredCallback }; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, params, function() {return null;}, version, endpointConfig, true /** Enable test mode */); assertEquals('recaptcha', recaptchaVerifier['type']); // Confirm property is readonly. recaptchaVerifier['type'] = 'modified'; assertEquals('recaptcha', recaptchaVerifier['type']); return recaptchaVerifier.render().then(function(widgetId) { // Mock instance should be rendered. assertEquals( 1, fireauth.GRecaptchaMockFactory.prototype.render.getCallCount()); // For visible reCAPTCHA, confirm expectedContainer element matches the // parent of the actual container. assertEquals( myElement, goog.dom.getParentElement( fireauth.GRecaptchaMockFactory.prototype.render.getLastCall() .getArgument(0))); var actualParams = fireauth.GRecaptchaMockFactory.prototype.render .getLastCall().getArgument(1); // Confirm expected parameters passed to reCAPTCHA. assertEquals('SITE_KEY', actualParams['sitekey']); assertEquals('light', actualParams['theme']); assertEquals('image', actualParams['type']); assertEquals('compact', actualParams['size']); assertEquals(startInstanceId, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(startInstanceId, widgetId); var verifyPromise = recaptchaVerifier.verify(); assertEquals(0, responseCallback.getCallCount()); clock.tick(solveTimeMs); assertEquals(1, responseCallback.getCallCount()); assertEquals('random0', responseCallback.getLastCall().getArgument(0)); return verifyPromise; }).then(function(recaptchaToken) { assertEquals('random0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(startInstanceId, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { // Same unexpired response returned. assertEquals('random0', recaptchaToken); assertEquals(0, expiredCallback.getCallCount()); // Expire response. clock.tick(expirationTimeMs); assertEquals(1, expiredCallback.getCallCount()); var resp = recaptchaVerifier.verify(); // Solve response after expiration. New reCAPTCHA token should be // returned. clock.tick(solveTimeMs); return resp; }).then(function(recaptchaToken) { assertEquals(2, responseCallback.getCallCount()); assertEquals('random1', responseCallback.getLastCall().getArgument(0)); assertEquals('random1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_render_invisible_testMode() { return installAndRunTest('testBaseVerifier_invisible_testMode', function() { // Confirm expected endpoint config and version passed to underlying RPC // handler. var version = '1.2.3'; var responseCallback = goog.testing.recordFunction(); var expiredCallback = goog.testing.recordFunction(); // Record calls to grecaptchaMock.render. stubs.replace( fireauth.GRecaptchaMockFactory.prototype, 'render', goog.testing.recordFunction( fireauth.GRecaptchaMockFactory.prototype.render)); var endpoint = fireauth.constants.Endpoint.STAGING; var endpointConfig = { 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, 'secureTokenEndpoint': endpoint.secureTokenEndpoint }; var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', endpointConfig, version) .$returns(rpcHandler); safeLoad(ignoreArgument).$never(); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); // Install mock clock. clock = new goog.testing.MockClock(true); var params = { 'size': 'invisible', 'theme': 'light', 'type': 'image', 'callback': responseCallback, 'expired-callback': expiredCallback }; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, params, function() {return null;}, version, endpointConfig, true /** Enable test mode */); assertEquals('recaptcha', recaptchaVerifier['type']); // Confirm property is readonly. recaptchaVerifier['type'] = 'modified'; assertEquals('recaptcha', recaptchaVerifier['type']); return recaptchaVerifier.render().then(function(widgetId) { // Confirm mock reCAPTCHA instance rendered. assertEquals( 1, fireauth.GRecaptchaMockFactory.prototype.render.getCallCount()); // For invisible reCAPTCHA, confirm expectedContainer element matches the // actual container. assertEquals( myElement, fireauth.GRecaptchaMockFactory.prototype.render.getLastCall() .getArgument(0)); var actualParams = fireauth.GRecaptchaMockFactory.prototype.render .getLastCall().getArgument(1); // Confirm expected parameters passed to reCAPTCHA. assertEquals('SITE_KEY', actualParams['sitekey']); assertEquals('light', actualParams['theme']); assertEquals('image', actualParams['type']); assertEquals('invisible', actualParams['size']); assertEquals(startInstanceId, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(startInstanceId, widgetId); var verifyPromise = recaptchaVerifier.verify(); // verify calls render underneath, wait for it to resolve before running // clock. return recaptchaVerifier.render().then(function() { assertEquals(0, responseCallback.getCallCount()); clock.tick(solveTimeMs); assertEquals(1, responseCallback.getCallCount()); assertEquals('random0', responseCallback.getLastCall().getArgument(0)); return verifyPromise; }); }).then(function(recaptchaToken) { assertEquals('random0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(startInstanceId, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { // Same unexpired response returned. assertEquals('random0', recaptchaToken); assertEquals(0, expiredCallback.getCallCount()); // Expire response. clock.tick(expirationTimeMs); assertEquals(1, expiredCallback.getCallCount()); // Element click should resolve with a token. goog.testing.events.fireClickSequence(myElement); clock.tick(solveTimeMs); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals(2, responseCallback.getCallCount()); assertEquals('random1', responseCallback.getLastCall().getArgument(0)); assertEquals('random1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_render_offline() { return installAndRunTest('testBaseAppVerifier_render_offline', function() { // Install mock clock. clock = new goog.testing.MockClock(true); var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var isOnline = mockControl.createMethodMock(fireauth.util, 'isOnline'); var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); // Simulate first attempt fails due to network connection not being // available. isOnline().$does(function() { goog.Promise.resolve().then(function() { clock.tick(5000); }); return false; }); // Simulate first call does nothing due to network timeout. safeLoad(ignoreArgument).$returns( new goog.Promise(function(resolve, reject) {})); // Simulate second attempt succeeding. isOnline().$returns(true); safeLoad(ignoreArgument) .$does(function(url) { var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); var callback = uri.getParameterValue('onload'); assertEquals('', uri.getParameterValue('hl')); initializeRecaptchaMocks(); goog.global[callback](); }) .$once(); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY', 'theme': 'light', 'type': 'image', // Invalid callback names should be ignored. 'callback': 'invalid', 'expired-callback': 'invalid' }; var expectedError = new fireauth.AuthError( fireauth.authenum.Error.NETWORK_REQUEST_FAILED); var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); return recaptchaVerifier.render().thenCatch(function(error) { // Initial attempt fails due to network connection. fireauth.common.testHelper.assertErrorEquals(expectedError, error); // Try again. It should be successful now. return recaptchaVerifier.render(); }).then(function(widgetId) { assertRecaptchaParams(widgetId, myElement, expectedParams); assertEquals(0, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); grecaptcha.solveResponse(0); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); grecaptcha.expireResponse(0); var resp = recaptchaVerifier.verify(); grecaptcha.solveResponse(0); return resp; }).then(function(recaptchaToken) { assertEquals('response-1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_render_grecaptchaLoaded() { return installAndRunTest('testBaseAppVerifier_recaptchaLoaded', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY' }; // In addition, test when the developer passes their own callbacks. var devCallback = goog.testing.recordFunction(); var devExpiredCallback = goog.testing.recordFunction(); var params = { 'callback': devCallback, 'expired-callback': devExpiredCallback }; var resp = null; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, params); return recaptchaVerifier.render().then(function(widgetId) { assertRecaptchaParams(widgetId, myElement, expectedParams); assertEquals(0, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); // Simulate the reCAPTCHA challenge solved. grecaptcha.solveResponse(0); // Developer callback should be called with the expected token. assertEquals(1, devCallback.getCallCount()); assertEquals('response-0', devCallback.getLastCall().getArgument(0)); // verify should resolve with the same token. return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { // Cached response returned. assertEquals(0, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Expire the response. grecaptcha.expireResponse(0); // Developer expired callback should be triggered. assertEquals(1, devExpiredCallback.getCallCount()); // Try to verify again. resp = recaptchaVerifier.verify(); // Break thread to allow the verification to pick up the new response. return goog.Promise.resolve(); }).then(function() { // Solve reCAPTCHA. Ths should be picked up. grecaptcha.solveResponse(0); // Developer token callback triggered with new token. assertEquals(2, devCallback.getCallCount()); assertEquals('response-1',devCallback.getLastCall().getArgument(0)); assertEquals('response-1', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Expire token. grecaptcha.expireResponse(0); // Expired callback triggered. assertEquals(2, devExpiredCallback.getCallCount()); // Solve reCAPTCHA. grecaptcha.solveResponse(0); // Developer callback triggered with the new token. assertEquals(3, devCallback.getCallCount()); assertEquals('response-2', devCallback.getLastCall().getArgument(0)); assertEquals('response-2', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Confirm the first resolved token picked up earlier. return resp; }).then(function(recaptchaToken) { assertEquals('response-1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_render_stringCallbacks() { return installAndRunTest('testBaseAppVerifier_stringCallbacks', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY' }; // Test when the developer passes callback function names instead of the // function references directly. goog.global['devCallback'] = goog.testing.recordFunction(); goog.global['devExpiredCallback'] = goog.testing.recordFunction(); var params = { 'callback': 'devCallback', 'expired-callback': 'devExpiredCallback' }; var resp = null; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement, params); return recaptchaVerifier.render().then(function(widgetId) { assertRecaptchaParams(widgetId, myElement, expectedParams); assertEquals(0, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { assertEquals(0, widgetId); // Simulate the reCAPTCHA challenge solved. grecaptcha.solveResponse(0); // Developer callback should be called with the expected token. assertEquals(1, goog.global['devCallback'].getCallCount()); assertEquals( 'response-0', goog.global['devCallback'].getLastCall().getArgument(0)); // verify should resolve with the same token. return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Already rendered. return recaptchaVerifier.render(); }).then(function(widgetId) { // Cached response returned. assertEquals(0, widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Expire the response. grecaptcha.expireResponse(0); // Developer expired callback should be triggered. assertEquals(1, goog.global['devExpiredCallback'].getCallCount()); // Try to verify again. resp = recaptchaVerifier.verify(); // Break thread to allow the verification to pick up the new response. return goog.Promise.resolve(); }).then(function() { // Solve reCAPTCHA. Ths should be picked up. grecaptcha.solveResponse(0); // Developer token callback triggered with new token. assertEquals(2, goog.global['devCallback'].getCallCount()); assertEquals( 'response-1', goog.global['devCallback'].getLastCall().getArgument(0)); assertEquals('response-1', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Expire token. grecaptcha.expireResponse(0); // Expired callback triggered. assertEquals(2, goog.global['devExpiredCallback'].getCallCount()); // Solve reCAPTCHA. grecaptcha.solveResponse(0); // Developer callback triggered with the new token. assertEquals(3, goog.global['devCallback'].getCallCount()); assertEquals( 'response-2', goog.global['devCallback'].getLastCall().getArgument(0)); assertEquals('response-2', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Confirm the first resolved token picked up earlier. return resp; }).then(function(recaptchaToken) { assertEquals('response-1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_getRecaptchaParamError() { return installAndRunTest( 'testBaseAppVerifier__recaptchaParamError', function() { var expectedError = new fireauth.AuthError( fireauth.authenum.Error.INTERNAL_ERROR, 'Something unexpected happened.'); var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); safeLoad(ignoreArgument) .$does(function(url) { var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); var callback = uri.getParameterValue('onload'); assertEquals('', uri.getParameterValue('hl')); initializeRecaptchaMocks(); goog.global[callback](); }) .$once(); // Simulate first attempt fails for some unknown reason. rpcHandler.getRecaptchaParam().$once().$does(function() { return goog.Promise.reject(expectedError); }); // Allow second attempt to succeed. rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY', 'theme': 'light', 'type': 'image' }; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); return recaptchaVerifier.render().thenCatch(function(error) { // First attempt fails with the same expected underlying error. fireauth.common.testHelper.assertErrorEquals(expectedError, error); // Try again. return recaptchaVerifier.render(); }).then(function(widgetId) { // Resolves with the expected widget ID. assertRecaptchaParams(widgetId, myElement, expectedParams); assertEquals(0, widgetId); return recaptchaVerifier.render(); }).then(function(widgetId) { // Same cached response. All other behavior should be the same. assertEquals(0, widgetId); }); }); } function testBaseRecaptchaVerifier_verify_newToken() { return installAndRunTest('testBaseAppVerifier_verify_newToken', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY', 'theme': 'light', 'type': 'image' }; var resp = null; var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); // Simulate challenge solved after calling verify. setTimeout(function() { grecaptcha.solveResponse(0); }, 10); // This should render the reCAPTCHA and then after challenge is solve, // resolve with the expected reCAPTCHA token. return recaptchaVerifier.verify().then(function(recaptchaToken) { assertRecaptchaParams(0, myElement, expectedParams); assertEquals('response-0', recaptchaToken); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // Expire the token response. grecaptcha.expireResponse(0); // Verify again. resp = recaptchaVerifier.verify(); return goog.Promise.resolve(); }).then(function() { // New response should be picked up. grecaptcha.solveResponse(0); assertEquals('response-1', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Expire and then solve again. grecaptcha.expireResponse(0); grecaptcha.solveResponse(0); assertEquals('response-2', grecaptcha.getResponse()); return goog.Promise.resolve(); }).then(function() { // Verify should resolve with the first expected response. return resp; }).then(function(recaptchaToken) { assertEquals('response-1', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_idElement() { return installAndRunTest('testBaseAppVerifier_idElement', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var expectedParams = { 'sitekey': 'SITE_KEY', 'theme': 'light', 'type': 'image' }; // Pass the element ID instead of the element itself for the container // argument. var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', 'recaptcha'); setTimeout(function() { grecaptcha.solveResponse(0); }, 10); return recaptchaVerifier.verify().then(function(recaptchaToken) { // reCAPTCHA initialized with the expected parameters. assertRecaptchaParams(0, 'recaptcha', expectedParams); // verify resolves with the expected response. assertEquals('response-0', recaptchaToken); // Same response cached until expiration. return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); }); }); } function testBaseRecaptchaVerifier_verify_reset() { return installAndRunTest('testBaseAppVerifier_verify_reset', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig = { 'recaptchaSiteKey': 'SITE_KEY' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); mockControl.$replayAll(); var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( 'API_KEY', myElement); var widgetIdReturned = null; return recaptchaVerifier.render().then(function(widgetId) { assertEquals(0, widgetId); widgetIdReturned = widgetId; grecaptcha.solveResponse(widgetId); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { assertEquals('response-0', recaptchaToken); // After reset, the cached response is forgotten and the new one is used. assertEquals('response-0', grecaptcha.getResponse(widgetIdReturned)); recaptchaVerifier.reset(); assertEquals('', grecaptcha.getResponse(widgetIdReturned)); setTimeout(function() { // The new solved challenge will be used as the old response is reset. grecaptcha.solveResponse(0); }, 10); return recaptchaVerifier.verify(); }).then(function(recaptchaToken) { // Since reCAPTCHA is reset, the new response should be used. assertEquals('response-2', recaptchaToken); assertEquals('response-2', grecaptcha.getResponse(0)); }); }); } function testBaseRecaptchaVerifier_multipleVerifiers() { return installAndRunTest('testBaseAppVerifier_multipleVerifiers', function() { // Simulate grecaptcha loaded. initializeRecaptchaMocks(); var recaptchaConfig1 = { 'recaptchaSiteKey': 'SITE_KEY1' }; var recaptchaConfig2 = { 'recaptchaSiteKey': 'SITE_KEY2' }; var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandler2 = mockControl.createStrictMock(fireauth.RpcHandler); var rpcHandlerConstructor = mockControl.createConstructorMock( fireauth, 'RpcHandler'); rpcHandlerConstructor('API_KEY', null, ignoreArgument) .$returns(rpcHandler); rpcHandlerConstructor('API_KEY', null, ignoreArgument) .$returns(rpcHandler2); rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig1); rpcHandler2.getRecaptchaParam().$once().$returns(recaptchaConfig2); mockControl.$replayAll(); var expectedParams1 = {