// Copyright 2011 The Closure Library Authors. All Rights Reserved. // // 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. goog.provide('goog.labs.net.xhrTest'); goog.setTestOnly('goog.labs.net.xhrTest'); goog.require('goog.Promise'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.labs.net.xhr'); goog.require('goog.net.WrapperXmlHttpFactory'); goog.require('goog.net.XmlHttp'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.TestCase'); goog.require('goog.testing.jsunit'); goog.require('goog.userAgent'); /** Path to a small download target used for testing binary requests. */ var TEST_IMAGE = 'testdata/cleardot.gif'; /** The expected bytes of the test image. */ var TEST_IMAGE_BYTES = [ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0xFF, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B ]; function setUpPage() { goog.testing.TestCase.getActiveTestCase().promiseTimeout = 10000; // 10s } function stubXhrToReturn(status, opt_responseText, opt_latency) { if (goog.isDefAndNotNull(opt_latency)) { mockClock = new goog.testing.MockClock(true); } var stubXhr = { sent: false, aborted: false, status: 0, headers: {}, open: function(method, url, async) { this.method = method; this.url = url; this.async = async; }, setRequestHeader: function(key, value) { this.headers[key] = value; }, overrideMimeType: function(mimeType) { this.mimeType = mimeType; }, abort: function() { this.aborted = true; this.load(0); }, send: function(data) { this.data = data; this.sent = true; // Fulfill the send asynchronously, or possibly with the MockClock. window.setTimeout(goog.bind(this.load, this, status), opt_latency || 0); if (mockClock) { mockClock.tick(opt_latency); } }, load: function(status) { this.status = status; if (goog.isDefAndNotNull(opt_responseText)) { this.responseText = opt_responseText; } this.readyState = 4; if (this.onreadystatechange) this.onreadystatechange(); } }; stubXmlHttpWith(stubXhr); } function stubXhrToThrow(err) { stubXmlHttpWith(buildThrowingStubXhr(err)); } function buildThrowingStubXhr(err) { return { sent: false, aborted: false, status: 0, headers: {}, open: function(method, url, async) { this.method = method; this.url = url; this.async = async; }, setRequestHeader: function(key, value) { this.headers[key] = value; }, overrideMimeType: function(mimeType) { this.mimeType = mimeType; }, send: function(data) { throw err; } }; } function stubXmlHttpWith(stubXhr) { goog.net.XmlHttp = function() { return stubXhr; }; for (var x in originalXmlHttp) { goog.net.XmlHttp[x] = originalXmlHttp[x]; } } var xhr = goog.labs.net.xhr; var originalXmlHttp = goog.net.XmlHttp; var mockClock; function tearDown() { if (mockClock) { mockClock.dispose(); mockClock = null; } goog.net.XmlHttp = originalXmlHttp; } /** * Tests whether the test was loaded from a file: protocol. Tests that use a * real network request cannot be run from the local file system due to * cross-origin restrictions, but will run if the tests are hosted on a server. * A log message is added to the test case to warn users that the a test was * skipped. * * @return {boolean} Whether the test is running on a local file system. */ function isRunningLocally() { if (window.location.protocol == 'file:') { var testCase = goog.global['G_testRunner'].testCase; testCase.saveMessage('Test skipped while running on local file system.'); return true; } return false; } function testSimpleRequest() { if (isRunningLocally()) return; return xhr.send('GET', 'testdata/xhr_test_text.data').then(function(xhr) { assertEquals('Just some data.', xhr.responseText); assertEquals(200, xhr.status); }); } function testGetText() { if (isRunningLocally()) return; return xhr.get('testdata/xhr_test_text.data').then(function(responseText) { assertEquals('Just some data.', responseText); }); } function testGetTextWithJson() { if (isRunningLocally()) return; return xhr.get('testdata/xhr_test_json.data').then(function(responseText) { assertEquals('while(1);\n{"stat":"ok","count":12345}\n', responseText); }); } function testPostText() { if (isRunningLocally()) return; return xhr.post('testdata/xhr_test_text.data', 'post-data') .then(function(responseText) { // No good way to test post-data gets transported. assertEquals('Just some data.', responseText); }); } function testGetJson() { if (isRunningLocally()) return; return xhr.getJson('testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'}) .then(function(responseObj) { assertEquals('ok', responseObj['stat']); assertEquals(12345, responseObj['count']); }); } function testGetBlob() { if (isRunningLocally()) return; // IE9 and earlier do not support blobs. if (!('Blob' in goog.global)) { var err = assertThrows(function() { xhr.getBlob(TEST_IMAGE); }); assertEquals( 'Assertion failed: getBlob is not supported in this browser.', err.message); return; } var options = {withCredentials: true}; return xhr.getBlob(TEST_IMAGE, options) .then(function(blob) { var reader = new FileReader(); return new goog.Promise(function(resolve, reject) { goog.events.listenOnce(reader, goog.events.EventType.LOAD, resolve); reader.readAsArrayBuffer(blob); }); }) .then(function(e) { assertElementsEquals(TEST_IMAGE_BYTES, new Uint8Array(e.target.result)); assertObjectEquals( 'input options should not have mutated.', {withCredentials: true}, options); }); } function testGetBytes() { if (isRunningLocally()) return; // IE8 requires a VBScript fallback to read the bytes from the response. if (goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) { var err = assertThrows(function() { xhr.getBytes(TEST_IMAGE); }); assertEquals( 'Assertion failed: getBytes is not supported in this browser.', err.message); return; } var options = {withCredentials: true}; return xhr.getBytes(TEST_IMAGE).then(function(bytes) { assertElementsEquals(TEST_IMAGE_BYTES, bytes); assertObjectEquals( 'input options should not have mutated.', {withCredentials: true}, options); }); } function testSerialRequests() { if (isRunningLocally()) return; return xhr.get('testdata/xhr_test_text.data') .then(function(response) { return xhr.getJson( 'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'}); }) .then(function(responseObj) { // Data that comes through to callbacks should be from the 2nd request. assertEquals('ok', responseObj['stat']); assertEquals(12345, responseObj['count']); }); } function testBadUrlDetectedAsError() { if (isRunningLocally()) return; return xhr.getJson('unknown-file.dat') .then(fail /* opt_onFulfilled */, function(err) { assertTrue( 'Error should be an HTTP error', err instanceof xhr.HttpError); assertEquals(404, err.status); assertNotNull(err.xhr); }); } function testBadOriginTriggersOnErrorHandler() { if (goog.userAgent.EDGE) return; // failing b/62677027 return xhr.get('http://www.google.com') .then( function() { fail( 'XHR to http://www.google.com should\'ve failed due to ' + 'same-origin policy.'); } /* opt_onFulfilled */, function(err) { // In IE this will be a goog.labs.net.xhr.Error since it is thrown // when calling xhr.open(), other browsers will raise an HttpError. assertTrue( 'Error should be an xhr error', err instanceof xhr.Error); assertNotNull(err.xhr); }); } //============================================================================ // The following tests use a stubbed out XMLHttpRequest. //============================================================================ function testSendNoOptions() { var called = false; stubXhrToReturn(200); assertFalse('Callback should not yet have been called', called); return xhr.send('GET', 'test-url', null).then(function(stubXhr) { called = true; assertEquals('GET', stubXhr.method); assertEquals('test-url', stubXhr.url); }); } function testSendPostSetsDefaultHeader() { stubXhrToReturn(200); return xhr.send('POST', 'test-url', null).then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals( 'application/x-www-form-urlencoded;charset=utf-8', stubXhr.headers['Content-Type']); }); } function testSendPostDoesntSetHeaderWithFormData() { if (!goog.global['FormData']) { return; } var formData = new goog.global['FormData'](); formData.append('name', 'value'); stubXhrToReturn(200); return xhr.send('POST', 'test-url', formData).then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals(undefined, stubXhr.headers['Content-Type']); }); } function testSendPostHeaders() { stubXhrToReturn(200); return xhr .send( 'POST', 'test-url', null, {headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'}}) .then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals('text/plain', stubXhr.headers['Content-Type']); assertEquals('FooBar', stubXhr.headers['X-Made-Up']); }); } function testSendPostHeadersWithFormData() { if (!goog.global['FormData']) { return; } var formData = new goog.global['FormData'](); formData.append('name', 'value'); stubXhrToReturn(200); return xhr .send( 'POST', 'test-url', formData, {headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'}}) .then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals('text/plain', stubXhr.headers['Content-Type']); assertEquals('FooBar', stubXhr.headers['X-Made-Up']); }); } function testSendNullPostHeaders() { stubXhrToReturn(200); return xhr .send('POST', 'test-url', null, { headers: {'Content-Type': null, 'X-Made-Up': 'FooBar', 'Y-Made-Up': null} }) .then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals(undefined, stubXhr.headers['Content-Type']); assertEquals('FooBar', stubXhr.headers['X-Made-Up']); assertEquals(undefined, stubXhr.headers['Y-Made-Up']); }); } function testSendNullPostHeadersWithFormData() { if (!goog.global['FormData']) { return; } var formData = new goog.global['FormData'](); formData.append('name', 'value'); stubXhrToReturn(200); return xhr .send('POST', 'test-url', formData, { headers: {'Content-Type': null, 'X-Made-Up': 'FooBar', 'Y-Made-Up': null} }) .then(function(stubXhr) { assertEquals('POST', stubXhr.method); assertEquals('test-url', stubXhr.url); assertEquals(undefined, stubXhr.headers['Content-Type']); assertEquals('FooBar', stubXhr.headers['X-Made-Up']); assertEquals(undefined, stubXhr.headers['Y-Made-Up']); }); } function testSendWithCredentials() { stubXhrToReturn(200); return xhr.send('POST', 'test-url', null, {withCredentials: true}) .then(function(stubXhr) { assertTrue('XHR should have been sent', stubXhr.sent); assertTrue(stubXhr.withCredentials); }); } function testSendWithMimeType() { stubXhrToReturn(200); return xhr.send('POST', 'test-url', null, {mimeType: 'text/plain'}) .then(function(stubXhr) { assertTrue('XHR should have been sent', stubXhr.sent); assertEquals('text/plain', stubXhr.mimeType); }); } function testSendWithHttpError() { stubXhrToReturn(500); return xhr.send('POST', 'test-url', null) .then(fail /* opt_onResolved */, function(err) { assertTrue(err instanceof xhr.HttpError); assertTrue(err.xhr.sent); assertEquals(500, err.status); }); } function testSendWithTimeoutNotHit() { stubXhrToReturn(200, null /* opt_responseText */, 1400 /* opt_latency */); return xhr.send('POST', 'test-url', null, {timeoutMs: 1500}) .then(function(stubXhr) { assertTrue(mockClock.getTimeoutsMade() > 0); assertTrue('XHR should have been sent', stubXhr.sent); assertFalse('XHR should not have been aborted', stubXhr.aborted); }); } function testSendWithTimeoutHit() { stubXhrToReturn(200, null /* opt_responseText */, 50 /* opt_latency */); return xhr.send('POST', 'test-url', null, {timeoutMs: 50}) .then(fail /* opt_onResolved */, function(err) { assertTrue('XHR should have been sent', err.xhr.sent); assertTrue('XHR should have been aborted', err.xhr.aborted); assertTrue(err instanceof xhr.TimeoutError); }); } function testCancelRequest() { stubXhrToReturn(200); var promise = xhr.send('GET', 'test-url') .then(fail /* opt_onResolved */, function(error) { assertTrue(error instanceof goog.Promise.CancellationError); return null; // Return a non-error value for the test runner. }); promise.cancel(); return promise; } function testGetJson() { var stubXhr = stubXhrToReturn(200, '{"a": 1, "b": 2}'); xhr.getJson('test-url').then(function(responseObj) { assertObjectEquals({a: 1, b: 2}, responseObj); }); } function testGetJsonWithXssiPrefix() { stubXhrToReturn(200, 'while(1);\n{"a": 1, "b": 2}'); return xhr.getJson('test-url', {xssiPrefix: 'while(1);\n'}) .then(function(responseObj) { assertObjectEquals({a: 1, b: 2}, responseObj); }); } function testSendWithClientException() { stubXhrToThrow(new Error('CORS XHR with file:// schemas not allowed.')); return xhr.send('POST', 'file://test-url', null) .then(fail /* opt_onResolved */, function(err) { assertFalse('XHR should not have been sent', err.xhr.sent); assertTrue(err instanceof Error); assertTrue( /CORS XHR with file:\/\/ schemas not allowed./.test(err.message)); }); } function testSendWithFactory() { stubXhrToReturn(200); var options = { xmlHttpFactory: new goog.net.WrapperXmlHttpFactory( goog.partial(buildThrowingStubXhr, new Error('Bad factory')), goog.net.XmlHttp.getOptions) }; return xhr.send('POST', 'file://test-url', null, options) .then(fail /* opt_onResolved */, function(err) { assertTrue(err instanceof Error); }); }