/** * @license * Copyright The Closure Library Authors. * SPDX-License-Identifier: Apache-2.0 */ goog.module('goog.net.JsonpTest'); goog.setTestOnly(); const Const = goog.require('goog.string.Const'); const Jsonp = goog.require('goog.net.Jsonp'); const PropertyReplacer = goog.require('goog.testing.PropertyReplacer'); const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl'); const googFunctions = goog.require('goog.functions'); const recordFunction = goog.require('goog.testing.recordFunction'); const safe = goog.require('goog.dom.safe'); const testSuite = goog.require('goog.testing.testSuite'); const userAgent = goog.require('goog.userAgent'); // Global vars to facilitate a shared set up function. let timeoutWasCalled; let timeoutHandler; const fakeUrl = 'https://fake-site.eek/'; const fakeTrustedUrl = TrustedResourceUrl.fromConstant(Const.from(fakeUrl)); let originalTimeout; // Firefox throws a JS error when a script is not found. We catch that here and // ensure the test case doesn't fail because of it. const originalOnError = window.onerror; window.onerror = (msg, url, line) => { // TODO(user): Safari 3 on the farm returns an object instead of the typical // params. Pass through errors for safari for now. if (userAgent.WEBKIT || msg == 'Error loading script' && url.indexOf('fake-site') != -1) { return true; } else { return originalOnError && originalOnError(msg, url, line); } }; // Quick function records the before-state of the DOM, and then return a // a function to check that XDC isn't leaving stuff behind. function newCleanupGuard() { const bodyChildCount = document.body.childNodes.length; return () => { // let any timeout queues finish before we check these: window.setTimeout(/** @suppress {checkTypes} suppression added to enable type checking */ () => { let propCounter = 0; // All callbacks should have been deleted or be the null // function. for (const key in globalThis) { // NOTES: callbacks are stored on globalThis with // property name prefixed with // goog.net.Jsonp.CALLBACKS. if (key.indexOf(Jsonp.CALLBACKS) == 0) { /** * @suppress {visibility} suppression added to * enable type checking */ const callbackId = Jsonp.getCallbackId_(key); if (globalThis[callbackId] && globalThis[callbackId] != googFunctions.UNDEFINED) { propCounter++; } } } assertEquals( 'script cleanup', bodyChildCount, document.body.childNodes.length); assertEquals( 'window jsonp array empty', 0, propCounter); }, 0); }; } /** @suppress {missingProperties} suppression added to enable type checking */ function getScriptElement(result) { return result.deferred_.defaultScope_.script_; } testSuite({ setUp() { timeoutWasCalled = false; timeoutHandler = null; originalTimeout = window.setTimeout; /** @suppress {missingReturn} suppression added to enable type checking */ window.setTimeout = (handler, time) => { timeoutWasCalled = true; timeoutHandler = handler; }; }, tearDown() { window.setTimeout = originalTimeout; }, // Check that send function is sane when things go well. /** @suppress {checkTypes} suppression added to enable type checking */ testSend() { let replyReceived; const jsonp = new Jsonp(fakeTrustedUrl); const checkCleanup = newCleanupGuard(); const userCallback = (data) => { replyReceived = data; }; const payload = {atisket: 'atasket', basket: 'yellow'}; const result = jsonp.send(payload, userCallback); const script = getScriptElement(result); assertNotNull('script created', script); assertEquals('encoding is utf-8', 'UTF-8', script.charset); // Check that the URL matches our payload. assertTrue('payload in url', script.src.indexOf('basket=yellow') > -1); assertTrue('server url', script.src.indexOf(fakeUrl) == 0); // Now, we have to track down the name of the callback function, so we can // call that to simulate a returned request + verify that the callback // function does not break if it receives a second unexpected parameter. const callbackName = /callback=([^&]+)/.exec(script.src)[1]; const callbackFunc = eval(callbackName); callbackFunc( {some: 'data', another: ['data', 'right', 'here']}, 'unexpected'); assertEquals('input was received', 'right', replyReceived.another[1]); // Because the callbackFunc calls cleanUp_ and that calls setTimeout which // we have overwritten, we have to call the timeoutHandler to actually do // the cleaning. timeoutHandler(); checkCleanup(); timeoutHandler(); }, // Check that send function is sane when things go well. /** @suppress {checkTypes} suppression added to enable type checking */ testSendWhenCallbackHasTwoParameters() { let replyReceived; let replyReceived2; const jsonp = new Jsonp(fakeTrustedUrl); const checkCleanup = newCleanupGuard(); const userCallback = (data, opt_data2) => { replyReceived = data; replyReceived2 = opt_data2; }; const payload = {atisket: 'atasket', basket: 'yellow'}; const result = jsonp.send(payload, userCallback); const script = getScriptElement(result); // Test a callback function that receives two parameters. const callbackName = /callback=([^&]+)/.exec(script.src)[1]; const callbackFunc = eval(callbackName); callbackFunc('param1', {some: 'data', another: ['data', 'right', 'here']}); assertEquals('input was received', 'param1', replyReceived); assertEquals( 'second input was received', 'right', replyReceived2.another[1]); // Because the callbackFunc calls cleanUp_ and that calls setTimeout which // we have overwritten, we have to call the timeoutHandler to actually do // the cleaning. timeoutHandler(); checkCleanup(); timeoutHandler(); }, // Check that send function works correctly when callback param value is // specified. /** @suppress {visibility,checkTypes} suppression added to enable type checking */ testSendWithCallbackParamValue() { let replyReceived; const jsonp = new Jsonp(fakeTrustedUrl); const checkCleanup = newCleanupGuard(); const userCallback = (data) => { replyReceived = data; }; const payload = {atisket: 'atasket', basket: 'yellow'}; const result = jsonp.send(payload, userCallback, undefined, 'dummyId'); const script = getScriptElement(result); assertNotNull('script created', script); assertEquals('encoding is utf-8', 'UTF-8', script.charset); // Check that the URL matches our payload. assertTrue('payload in url', script.src.indexOf('basket=yellow') > -1); assertTrue( 'dummyId in url', script.src.indexOf('callback=' + Jsonp.getCallbackId_('dummyId')) > -1); assertTrue('server url', script.src.indexOf(fakeUrl) == 0); // Now, we simulate a returned request using the known callback function // name. /** @suppress {visibility} suppression added to enable type checking */ const callbackFunc = eval('window.callback=' + Jsonp.getCallbackId_('dummyId')); callbackFunc({some: 'data', another: ['data', 'right', 'here']}); assertEquals('input was received', 'right', replyReceived.another[1]); // Because the callbackFunc calls cleanUp_ and that calls setTimeout which // we have overwritten, we have to call the timeoutHandler to actually do // the cleaning. timeoutHandler(); checkCleanup(); timeoutHandler(); }, // Check that the send function is sane when the thing goes south. /** @suppress {checkTypes} suppression added to enable type checking */ testSendFailure() { let replyReceived = false; let errorReplyReceived = false; const jsonp = new Jsonp(fakeTrustedUrl); const checkCleanup = newCleanupGuard(); const userCallback = (data) => { replyReceived = data; }; const userErrorCallback = (data) => { errorReplyReceived = data; }; const payload = {justa: 'test'}; jsonp.send(payload, userCallback, userErrorCallback); assertTrue('timeout called', timeoutWasCalled); // Now, simulate the time running out, so we go into error mode. // After jsonp.send(), the timeoutHandler now is the Jsonp.cleanUp_ // function. timeoutHandler(); // But that function also calls a setTimeout(), so it changes the timeout // handler once again, so to actually clean up we have to call the // timeoutHandler() once again. Fun! timeoutHandler(); assertFalse('standard callback not called', replyReceived); // The user's error handler should be called back with the same payload // passed back to it. assertEquals('error handler called', 'test', errorReplyReceived.justa); // Check that the relevant cleanup has occurred. checkCleanup(); // Check cleanup just calls setTimeout so we have to call the handler to // actually check that the cleanup worked. timeoutHandler(); }, // Check that a cancel call works and cleans up after itself. /** @suppress {checkTypes} suppression added to enable type checking */ testCancel() { const checkCleanup = newCleanupGuard(); let successCalled = false; const successCallback = () => { successCalled = true; }; // Send and cancel a request, then make sure it was cleaned up. const jsonp = new Jsonp(fakeTrustedUrl); const requestObject = jsonp.send({test: 'foo'}, successCallback); jsonp.cancel(requestObject); for (const key in globalThis[Jsonp.CALLBACKS]) { // NOTES: callbacks are stored on globalThis with property // name prefixed with goog.net.Jsonp.CALLBACKS. if (key.indexOf('goog.net.Jsonp.CALLBACKS') == 0) { /** @suppress {visibility} suppression added to enable type checking */ const callbackId = Jsonp.getCallbackId_(key); assertNotEquals( 'The success callback should have been removed', globalThis[callbackId], successCallback); } } // Make sure cancelling removes the script tag checkCleanup(); timeoutHandler(); }, /** @suppress {checkTypes} suppression added to enable type checking */ testPayloadParameters() { const checkCleanup = newCleanupGuard(); const jsonp = new Jsonp(fakeTrustedUrl); const result = jsonp.send({'foo': 3, 'bar': 'baz'}); const script = getScriptElement(result); assertEquals( 'Payload parameters should have been added to url.', `${fakeUrl}?foo=3&bar=baz`, script.src); checkCleanup(); timeoutHandler(); }, /** @suppress {checkTypes} suppression added to enable type checking */ testNonce() { const checkCleanup = newCleanupGuard(); const jsonp = new Jsonp(fakeTrustedUrl); let nonce = safe.getScriptNonce(); if (!nonce) { nonce = 'foo'; } jsonp.setNonce(nonce); const result = jsonp.send(); const script = getScriptElement(result); assertEquals( 'Nonce attribute should have been added to script element.', nonce, (script['nonce'] || script.getAttribute('nonce'))); checkCleanup(); timeoutHandler(); }, testOptionalPayload() { const checkCleanup = newCleanupGuard(); const errorCallback = recordFunction(); const stubs = new PropertyReplacer(); stubs.set(globalThis, 'setTimeout', (errorHandler) => { errorHandler(); }); const jsonp = new Jsonp(fakeTrustedUrl); const result = jsonp.send(null, null, errorCallback); const script = getScriptElement(result); assertEquals( 'Parameters should not have been added to url.', fakeUrl, script.src); // Clear the script hooks because we triggered the error manually. script.onload = () => {}; script.onerror = () => {}; script.onreadystatechange = () => {}; const errorCallbackArguments = errorCallback.getLastCall().getArguments(); assertEquals(1, errorCallbackArguments.length); assertObjectEquals({}, errorCallbackArguments[0]); checkCleanup(); stubs.reset(); }, });