/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/unit/test-xhr.js
1 147 строк
36 KB
Daniel Rozenberg
❄️ Unskip some tests that no longer fail (#39329)
09 авг 2023, 22:33
Не верифицирован
09 авг 2023, 22:33
1986291
Код
Авторство
О чём код?
import {utf8Decode} from '#core/types/string/bytes'; import {toggleExperiment} from '#experiments'; import {fetchPolyfill} from '#polyfills/fetch'; import {Services} from '#service'; import {xhrServiceForTesting} from '#service/xhr-impl'; import {user} from '#utils/log'; import {assertSuccess} from '#utils/xhr-utils'; import {getCookie} from '../../src/cookies'; import {createFormDataWrapper} from '../../src/form-data-wrapper'; import * as mode from '../../src/mode'; describes.sandboxed('XHR', {}, function (env) { let ampdocServiceForStub; let ampdoc; let ampdocViewerStub; let xhrCreated; let viewer; const location = {href: 'https://acme.com/path'}; const nativeWin = { location, fetch: window.fetch, }; const polyfillWin = { location, fetch: fetchPolyfill, }; // Given XHR calls give tests more time. this.timeout(5000); const scenarios = [ { win: nativeWin, desc: 'Native', }, { win: polyfillWin, desc: 'Polyfill', }, ]; function setupMockXhr() { const mockXhr = env.sandbox.useFakeXMLHttpRequest(); xhrCreated = new Promise((resolve) => (mockXhr.onCreate = resolve)); } function noOrigin(url) { let index = url.indexOf('//'); if (index == -1) { return url; } index = url.indexOf('/', index + 2); return url.substring(index); } beforeEach(() => { ampdocServiceForStub = env.sandbox.stub(Services, 'ampdocServiceFor'); ampdoc = { getRootNode: () => null, whenFirstVisible: () => Promise.resolve(), }; ampdocServiceForStub.returns({ isSingleDoc: () => false, getAmpDoc: () => ampdoc, getSingleDoc: () => ampdoc, }); ampdocViewerStub = env.sandbox.stub(Services, 'viewerForDoc'); ampdocViewerStub.returns({}); location.href = 'https://acme.com/path'; }); scenarios.forEach((test) => { let xhr; beforeEach(() => { xhr = xhrServiceForTesting(test.win); }); // Since it's the Native fetch, it won't use the XHR object so // mocking and testing the request becomes not doable. if (test.desc != 'Native') { describe('#XHR', () => { beforeEach(() => { setupMockXhr(); }); it('should allow GET and POST methods', () => { const get = xhr.fetchJson.bind(xhr, '/get?k=v1'); const post = xhr.fetchJson.bind(xhr, '/post', { method: 'POST', body: { hello: 'world', }, }); const put = xhr.fetchJson.bind(xhr, '/put', { method: 'PUT', body: { hello: 'world', }, }); const patch = xhr.fetchJson.bind(xhr, '/patch', { method: 'PATCH', body: { hello: 'world', }, }); const deleteMethod = xhr.fetchJson.bind(xhr, '/delete', { method: 'DELETE', body: { id: 3, }, }); expect(get).to.not.throw(); expect(post).to.not.throw(); allowConsoleError(() => { expect(put).to.throw(); }); allowConsoleError(() => { expect(patch).to.throw(); }); allowConsoleError(() => { expect(deleteMethod).to.throw(); }); }); it('should allow FormData as body', () => { const fakeWin = null; env.sandbox.stub(Services, 'platformFor').returns({ isIos() { return false; }, }); const formData = createFormDataWrapper(fakeWin); env.sandbox.stub(JSON, 'stringify'); formData.append('name', 'John Miller'); formData.append('age', 56); const post = xhr.fetchJson.bind(xhr, '/post', { method: 'POST', body: formData, }); expect(post).to.not.throw(); expect(JSON.stringify.called).to.be.false; }); it('should do `GET` as default method', () => { xhr.fetchJson('/get?k=v1'); return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); }); it('should normalize GET method name to uppercase', () => { xhr.fetchJson('/abc'); return xhrCreated.then((xhr) => expect(xhr.method).to.equal('GET')); }); it('should normalize POST method name to uppercase', () => { xhr.fetchJson('/abc', { method: 'post', body: { hello: 'world', }, }); return xhrCreated.then((xhr) => expect(xhr.method).to.equal('POST')); }); it('should inject source origin query parameter', () => { xhr.fetchJson('/get?k=v1#h1'); return xhrCreated.then((xhr) => expect(noOrigin(xhr.url)).to.equal( '/get?k=v1&__amp_source_origin=https%3A%2F%2Facme.com#h1' ) ); }); it('should inject source origin query parameter w/o query', () => { xhr.fetchJson('/get'); return xhrCreated.then((xhr) => expect(noOrigin(xhr.url)).to.equal( '/get?__amp_source_origin=https%3A%2F%2Facme.com' ) ); }); it('should defend against invalid source origin query parameter', () => { allowConsoleError(() => { expect(() => { xhr.fetchJson('/get?k=v1&__amp_source_origin=invalid#h1'); }).to.throw(/Source origin is not allowed/); }); }); it('should defend against empty source origin query parameter', () => { allowConsoleError(() => { expect(() => { xhr.fetchJson('/get?k=v1&__amp_source_origin=#h1'); }).to.throw(/Source origin is not allowed/); }); }); it('should defend against re-encoded source origin parameter', () => { allowConsoleError(() => { expect(() => { xhr.fetchJson('/get?k=v1&_%5famp_source_origin=#h1'); }).to.throw(/Source origin is not allowed/); }); }); it('should not include __amp_source_origin if ampCors set to false', () => { xhr.fetchJson('/get', {ampCors: false}); return xhrCreated.then((xhr) => expect(noOrigin(xhr.url)).to.equal('/get') ); }); it('should accept AMP origin when received in response', () => { const promise = xhr.fetchJson('/get'); xhrCreated.then((xhr) => xhr.respond( 200, { 'Content-Type': 'application/json', }, '{}' ) ); return promise; }); describe('doc visibility', () => { afterEach(() => { test.win.fetch.restore(); }); it('should not call fetch if view is not visible ', () => { const fetchCall = env.sandbox.spy(test.win, 'fetch'); ampdoc.whenFirstVisible = () => Promise.reject(); xhr.fetchJson('/get', {ampCors: false}); expect(fetchCall.notCalled).to.be.true; }); it('should call fetch if view is visible ', () => { const fetchCall = env.sandbox.spy(test.win, 'fetch'); ampdoc.whenFirstVisible = () => Promise.resolve(); const fetch = xhr.fetchJson('/get', {ampCors: false}); fetch.then(() => { expect(fetchCall.calledOnce).to.be.true; }); }); }); }); } describe('AMP-Same-Origin', () => { beforeEach(() => { xhr = xhrServiceForTesting(test.win); }); it('should not be set for cross origin requests', () => { const init = {}; xhr.fetchJson('/whatever', init); expect(init['headers']['AMP-Same-Origin']).to.be.undefined; }); it('should be set for all same origin GET requests', () => { const init = {}; location.href = '/path'; xhr.fetchJson('/whatever', init); expect(init['headers']['AMP-Same-Origin']).to.equal('true'); }); it('should be set for all same origin POST requests', () => { const init = {method: 'post', body: {}}; location.href = '/path'; xhr.fetchJson('/whatever', init); expect(init['headers']['AMP-Same-Origin']).to.equal('true'); }); it('should check origin not source origin', () => { let init = {method: 'post', body: {}}; location.href = 'https://cdn.ampproject.org/c/s/example.com/hello/path'; xhr.fetchJson('https://example.com/what/ever', init); expect(init['headers']['AMP-Same-Origin']).to.be.undefined; init = {method: 'post', body: {}}; location.href = 'https://example.com/hello/path'; xhr.fetchJson('https://example.com/what/ever', init); expect(init['headers']['AMP-Same-Origin']).to.equal('true'); }); }); describe(test.desc, () => { const {testServerPort} = window.ampTestRuntimeConfig; const baseUrl = `http://localhost:${testServerPort}`; beforeEach(() => { xhr = xhrServiceForTesting(test.win); }); describe('assertSuccess', () => { function createResponseInstance(body, init) { return new Response(body, init); } const mockXhr = { status: 200, headers: { 'Content-Type': 'plain/text', }, getResponseHeader: () => '', }; it('should resolve if success', () => { mockXhr.status = 200; return assertSuccess(createResponseInstance('', mockXhr)).then( (response) => { expect(response.status).to.equal(200); } ).should.not.be.rejected; }); it('should reject if error', () => { mockXhr.status = 500; return assertSuccess(createResponseInstance('', mockXhr)).should.be .rejected; }); it('should include response in error', () => { mockXhr.status = 500; return assertSuccess(createResponseInstance('', mockXhr)).catch( (error) => { expect(error.response).to.exist; expect(error.response.status).to.equal(500); } ); }); it('should not resolve after rejecting promise', () => { mockXhr.status = 500; mockXhr.responseText = '{"a": "hello"}'; mockXhr.headers['Content-Type'] = 'application/json'; mockXhr.getResponseHeader = () => 'application/json'; return assertSuccess(createResponseInstance('{"a": 2}', mockXhr)) .should.not.be.fulfilled; }); }); it('should do simple JSON fetch', () => { env.sandbox.stub(user(), 'assert'); return xhr .fetchJson(`${baseUrl}/get?k=v1`) .then((res) => res.json()) .then((res) => { expect(res).to.exist; expect(res['args']['k']).to.equal('v1'); }); }); it('should redirect fetch', () => { const url = `${baseUrl}/redirect-to?url=` + encodeURIComponent(`${baseUrl}/get?k=v2`); return xhr .fetchJson(url, {ampCors: false}) .then((res) => res.json()) .then((res) => { expect(res).to.exist; expect(res['args']['k']).to.equal('v2'); }); }); it('should fail fetch for 400-error', () => { const url = `${baseUrl}/status/404`; return xhr.fetchJson(url).then( () => { throw new Error('UNREACHABLE'); }, (error) => { expect(error.message).to.contain('HTTP error 404'); } ); }); it('should fail fetch for 500-error', () => { const url = `${baseUrl}/status/500?CID=cid`; return xhr.fetchJson(url).then( () => { throw new Error('UNREACHABLE'); }, (error) => { expect(error.message).to.contain('HTTP error 500'); } ); }); it('should NOT succeed CORS setting cookies without credentials', () => { const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); const url = `${baseUrl}/cookies/set?${cookieName}=v1`; return xhr.fetchJson(url).then((res) => { expect(res).to.exist; expect(getCookie(window, cookieName)).to.be.null; }); }); it('should succeed CORS setting cookies with credentials', () => { const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); const url = `${baseUrl}/cookies/set?${cookieName}=v1`; return xhr.fetchJson(url, {credentials: 'include'}).then((res) => { expect(res).to.exist; expect(getCookie(window, cookieName)).to.equal('v1'); }); }); it('should ignore CORS setting cookies w/omit credentials', () => { const cookieName = 'TEST_CORS_' + Math.round(Math.random() * 10000); const url = `${baseUrl}/cookies/set?${cookieName}=v1`; return xhr.fetchJson(url, {credentials: 'omit'}).then((res) => { expect(res).to.exist; expect(getCookie(window, cookieName)).to.be.null; }); }); it('should NOT succeed CORS with invalid credentials', () => { allowConsoleError(() => { expect(() => { xhr.fetchJson('https://acme.org/', {credentials: null}); }).to.throw(/Only credentials=include|omit support: null/); }); }); it('should expose HTTP headers', () => { const url = `${baseUrl}/response-headers?AMP-Header=Value1&` + 'Access-Control-Expose-Headers=AMP-Header'; return xhr.fetchAmpCors_(url, {ampCors: false}).then((res) => { expect(res.headers.get('AMP-Header')).to.equal('Value1'); }); }); it('should omit request details for privacy', () => { // NOTE THIS IS A BAD PORT ON PURPOSE. return xhr.fetchJson('http://localhost:31863/status/500').then( () => { throw new Error('UNREACHABLE'); }, (error) => { const {message} = error; expect(message).to.contain('http://localhost:31863'); expect(message).not.to.contain('status/500'); expect(message).not.to.contain('CID'); } ); }); }); describe('#fetchText', () => { const TEST_TEXT = 'test text'; let fetchStub; beforeEach(() => { xhr = xhrServiceForTesting(test.win); fetchStub = env.sandbox .stub(xhr, 'fetchAmpCors_') .callsFake(() => Promise.resolve(new Response(TEST_TEXT))); }); it('should be able to fetch a document', () => { const promise = xhr.fetchText('/text.html'); expect(fetchStub).to.be.calledWith('/text.html', { method: 'GET', headers: {'Accept': 'text/plain'}, }); return promise .then((res) => { return res.text(); }) .then((text) => { expect(text).to.equal(TEST_TEXT); }); }); }); describe('#fetch ' + test.desc, () => { const creative = '<html><body>This is a creative简</body></html>'; beforeEach(() => (xhr = xhrServiceForTesting(test.win))); // Using the Native fetch, we can't mock the XHR request, so an actual // HTTP request would be sent to the server. Only execute this test // when we're on the PolyFill case, so that we can mock the XHR and // control the response. if (test.desc != 'Native') { it('should be able to fetch a response', () => { setupMockXhr(); const promise = xhr.fetch('/index.html').then((response) => { expect(response.headers.get('X-foo-header')).to.equal('foo data'); expect(response.headers.get('X-bar-header')).to.equal('bar data'); response .arrayBuffer() .then((bytes) => utf8Decode(bytes)) .then((text) => { expect(text).to.equal(creative); }); }); xhrCreated.then((xhr) => xhr.respond( 200, { 'Content-Type': 'text/xml', 'X-foo-header': 'foo data', 'X-bar-header': 'bar data', }, creative ) ); return promise; }); } }); }); scenarios.forEach((test) => { const {testServerPort} = window.ampTestRuntimeConfig; const url = `http://localhost:${testServerPort}/post`; describe(test.desc + ' POST', () => { let xhr; beforeEach(() => (xhr = xhrServiceForTesting(test.win))); if (test.desc != 'Native') { it('should have required json POST headers by default', () => { setupMockXhr(); xhr.fetchJson(url, { method: 'POST', body: { hello: 'world', }, headers: { 'Other': 'another', }, }); return xhrCreated.then((xhr) => expect(xhr.requestHeaders).to.deep.equal({ 'Accept': 'application/json', 'Content-Type': 'text/plain;charset=utf-8', 'Other': 'another', // Not removed when other headers set. }) ); }); } it("should get an echo'd response back", () => { return xhr .fetchJson(url, { method: 'POST', body: { hello: 'world', }, headers: { 'Content-Type': 'application/json;charset=utf-8', }, }) .then((res) => res.json()) .then((res) => { expect(res.json).to.jsonEqual({ hello: 'world', }); }); }); it('should throw when `body` is not an object or array', () => { const objectFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: { hello: 'world', }, }); const arrayFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: ['hello', 'world'], }); const stringFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: 'hello world', }); const numberFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: 3, }); const booleanFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: true, }); const nullFn = xhr.fetchJson.bind(xhr, url, { method: 'POST', body: null, }); expect(objectFn).to.not.throw(); expect(arrayFn).to.not.throw(); allowConsoleError(() => { expect(stringFn).to.throw(); }); allowConsoleError(() => { expect(numberFn).to.throw(); }); allowConsoleError(() => { expect(booleanFn).to.throw(); }); allowConsoleError(() => { expect(nullFn).to.throw(); }); }); }); }); describe('interceptor', () => { const origin = 'https://acme.com'; let interceptionEnabledWin; let optedInDoc; let sendMessageStub; function getDefaultResponseOptions() { return { headers: [], }; } function getDefaultResponsePromise() { return Promise.resolve({init: getDefaultResponseOptions()}); } beforeEach(() => { optedInDoc = window.document.implementation.createHTMLDocument(''); optedInDoc.documentElement.setAttribute('allow-xhr-interception', ''); const ampdoc = { getRootNode: () => optedInDoc, whenFirstVisible: () => Promise.resolve(), }; ampdocServiceForStub.returns({ isSingleDoc: () => true, getAmpDoc: () => ampdoc, getSingleDoc: () => ampdoc, }); viewer = { hasCapability: () => true, isTrustedViewer: () => Promise.resolve(true), sendMessageAwaitResponse: getDefaultResponsePromise, whenFirstVisible: () => Promise.resolve(), }; sendMessageStub = env.sandbox.stub(viewer, 'sendMessageAwaitResponse'); sendMessageStub.returns(getDefaultResponsePromise()); ampdocViewerStub.returns(viewer); interceptionEnabledWin = { location: { href: `${origin}/path`, }, fetch: () => Promise.resolve(new Response('', getDefaultResponseOptions())), }; }); afterEach(() => { toggleExperiment( interceptionEnabledWin, 'untrusted-xhr-interception', false ); }); it('should not intercept if AMP doc is not single', () => { const ampdoc = { getRootNode: () => optedInDoc, whenFirstVisible: () => Promise.resolve(), }; ampdocServiceForStub.returns({ isSingleDoc: () => false, getAmpDoc: () => ampdoc, getSingleDoc: () => ampdoc, }); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://cdn.ampproject.org') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should not intercept if AMP doc does not opt in', () => { const nonOptedInDoc = window.document.implementation.createHTMLDocument(''); const ampdoc = { getRootNode: () => nonOptedInDoc, whenFirstVisible: () => Promise.resolve(), }; ampdocServiceForStub.returns({ isSingleDoc: () => true, getAmpDoc: () => ampdoc, getSingleDoc: () => ampdoc, }); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://cdn.ampproject.org') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should not intercept if viewer is not capable', () => { env.sandbox .stub(viewer, 'hasCapability') .withArgs('xhrInterceptor') .returns(false); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://cdn.ampproject.org') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should not intercept if viewer untrusted and non-dev mode', () => { env.sandbox .stub(viewer, 'isTrustedViewer') .returns(Promise.resolve(false)); interceptionEnabledWin.AMP_DEV_MODE = false; const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://cdn.ampproject.org') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should not intercept a 1p cdn from subdomain', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://subdomain-model.cdn.ampproject.org/ww.js') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should not intercept a 1p cdn resource', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://cdn.ampproject.org/ww.js') .then(() => expect(sendMessageStub).to.not.have.been.called); }); it('should intercept if viewer untrusted but in local dev mode', () => { env.sandbox .stub(viewer, 'isTrustedViewer') .returns(Promise.resolve(false)); env.sandbox.stub(mode, 'getMode').returns({localDev: true}); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then(() => expect(sendMessageStub).to.have.been.called); }); it('should intercept if untrusted-xhr-interception experiment enabled', () => { env.sandbox .stub(viewer, 'isTrustedViewer') .returns(Promise.resolve(false)); env.sandbox.stub(mode, 'getMode').returns({localDev: false}); env.sandbox .stub(viewer, 'hasCapability') .withArgs('xhrInterceptor') .returns(true); toggleExperiment( interceptionEnabledWin, 'untrusted-xhr-interception', true ); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then(() => expect(sendMessageStub).to.have.been.called); }); it('should intercept if non-dev mode but viewer trusted', () => { env.sandbox .stub(viewer, 'isTrustedViewer') .returns(Promise.resolve(true)); interceptionEnabledWin.AMP_DEV_MODE = false; const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then(() => expect(sendMessageStub).to.have.been.called); }); it('should send viewer message named `xhr`', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then(() => expect(sendMessageStub).to.have.been.calledWithMatch( 'xhr', env.sandbox.match.any ) ); }); it('should post correct structurally-cloneable GET request', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr.fetch('https://www.some-url.org/some-resource/').then(() => expect(sendMessageStub).to.have.been.calledWithMatch( env.sandbox.match.any, { originalRequest: { input: 'https://www.some-url.org/some-resource/' + '?__amp_source_origin=https%3A%2F%2Facme.com', init: { headers: {}, method: 'GET', }, }, } ) ); }); it('should post correct structurally-cloneable JSON request', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', { method: 'POST', headers: {'Content-Type': 'application/json;charset=utf-8'}, body: JSON.stringify({a: 42, b: [24, true]}), }) .then(() => expect(sendMessageStub).to.have.been.calledWithMatch( env.sandbox.match.any, { originalRequest: { input: 'https://www.some-url.org/some-resource/' + '?__amp_source_origin=https%3A%2F%2Facme.com', init: { headers: { 'Content-Type': 'application/json;charset=utf-8', }, body: '{"a":42,"b":[24,true]}', method: 'POST', }, }, } ) ); }); it('should post correct structurally-cloneable FormData request', () => { const xhr = xhrServiceForTesting(interceptionEnabledWin); const fakeWin = null; env.sandbox.stub(Services, 'platformFor').returns({ isIos() { return false; }, }); const formData = createFormDataWrapper(fakeWin); formData.append('a', 42); formData.append('b', '24'); formData.append('b', true); return xhr .fetch('https://www.some-url.org/some-resource/', { method: 'POST', body: formData, }) .then(() => expect(sendMessageStub).to.have.been.calledWithMatch( env.sandbox.match.any, { originalRequest: { input: 'https://www.some-url.org/some-resource/' + '?__amp_source_origin=https%3A%2F%2Facme.com', init: { headers: { 'Content-Type': 'multipart/form-data;charset=utf-8', }, body: [ ['a', '42'], ['b', '24'], ['b', 'true'], ], method: 'POST', }, }, } ) ); }); it('should be rejected when response undefined', () => { expectAsyncConsoleError(/Object expected/); sendMessageStub.returns(Promise.resolve()); const xhr = xhrServiceForTesting(interceptionEnabledWin); return expect( xhr.fetch('https://www.some-url.org/some-resource/') ).to.eventually.be.rejectedWith(Error, 'Object expected: undefined'); }); it('should be rejected when response null', () => { expectAsyncConsoleError(/Object expected/); sendMessageStub.returns(Promise.resolve(null)); const xhr = xhrServiceForTesting(interceptionEnabledWin); return expect( xhr.fetch('https://www.some-url.org/some-resource/') ).to.eventually.be.rejectedWith(Error, 'Object expected: null'); }); it('should be rejected when response is string', () => { expectAsyncConsoleError(/Object expected/); sendMessageStub.returns(Promise.resolve('response text')); const xhr = xhrServiceForTesting(interceptionEnabledWin); return expect( xhr.fetch('https://www.some-url.org/some-resource/') ).to.eventually.be.rejectedWith(Error, 'Object expected: response text'); }); describe('when native Response type is available', () => { beforeEach(() => (interceptionEnabledWin.Response = window.Response)); it('should return correct non-document response', () => { sendMessageStub.returns( Promise.resolve({ body: '{"content":32}', init: { status: 242, statusText: 'Magic status', headers: [ ['a', 2], ['b', false], ], }, }) ); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then((response) => { expect(response.headers.get('a')).to.equal('2'); expect(response.headers.get('b')).to.equal('false'); expect(response).to.have.property('ok').that.is.true; expect(response).to.have.property('status').that.equals(242); expect(response) .to.have.property('statusText') .that.equals('Magic status'); return expect(response.text()).to.eventually.equal( '{"content":32}' ); return expect(response.json()).to.eventually.deep.equal({ content: 32, }); }); }); }); describe('when native Response type is unavailable', () => { beforeEach(() => (interceptionEnabledWin.Response = undefined)); it('should return correct non-document response', () => { sendMessageStub.returns( Promise.resolve({ body: '{"content":32}', init: { status: 242, statusText: 'Magic status', headers: [ ['a', 2], ['b', false], ], }, }) ); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/') .then((response) => { expect(response.headers.get('a')).to.equal('2'); expect(response.headers.get('b')).to.equal('false'); expect(response).to.have.property('ok').that.is.true; expect(response).to.have.property('status').that.equals(242); return expect(response.json()).to.eventually.deep.equal({ content: 32, }); }); }); it('should return default response when body/init missing', () => { sendMessageStub.returns(Promise.resolve({})); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { expect(response.headers.get('a')).to.be.null; expect(response.headers.has('a')).to.be.false; expect(response).to.have.property('ok').that.is.true; expect(response).to.have.property('status').that.equals(200); return expect(response.text()).to.eventually.be.empty; }); }); it('should return default response when status/headers missing', () => { sendMessageStub.returns(Promise.resolve({body: '', init: {}})); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { expect(response.headers.get('a')).to.be.null; expect(response.headers.has('a')).to.be.false; expect(response).to.have.property('status').that.equals(200); }); }); it('should convert body to string', () => { sendMessageStub.returns(Promise.resolve({body: 32})); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { return expect(response.text()).to.eventually.equal('32'); }); }); it('should convert status to int', () => { sendMessageStub.returns(Promise.resolve({init: {status: '209.6'}})); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { return expect(response).to.have.property('status').that.equals(209); }); }); it('should convert headers to string', () => { sendMessageStub.returns( Promise.resolve({ init: { headers: [ [1, true], [false, NaN], [undefined, null], ], }, }) ); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { expect(response.headers.get(1)).to.equal('true'); expect(response.headers.get('false')).to.equal('NaN'); expect(response.headers.get('undefined')).to.equal('null'); }); }); it('should support case-insensitive header search', () => { sendMessageStub.returns( Promise.resolve({ init: { headers: [ ['Content-Type', 'text/plain'], ['ACCEPT', 'text/plain'], ['x-amp-custom', 'foo'], ], }, }) ); const xhr = xhrServiceForTesting(interceptionEnabledWin); return xhr .fetch('https://www.some-url.org/some-resource/', {ampCors: false}) .then((response) => { expect(response.headers.get('content-type')).to.equal('text/plain'); expect(response.headers.get('Accept')).to.equal('text/plain'); expect(response.headers.get('X-AMP-CUSTOM')).to.equal('foo'); }); }); }); }); describe('#xssiJson', () => { let xhr; beforeEach(() => { xhr = xhrServiceForTesting(polyfillWin); setupMockXhr(); }); it('should call response.json() if prefix is either missing or the empty string', () => { xhrCreated.then((mock) => mock.respond(200, [], '{a: 1}')); const response = { json: () => Promise.resolve(), text: () => Promise.reject(new Error('should not be called')), }; return Promise.all([xhr.xssiJson(response), xhr.xssiJson(response, '')]); }); it('should not strip characters if the prefix is not present', () => { xhrCreated.then((mock) => mock.respond(200, [], '{"a": 1}')); return xhr .fetchJson('/abc') .then((res) => xhr.xssiJson(res, 'while(1)')) .then((json) => { expect(json).to.be.deep.equal({a: 1}); }); }); it('should strip prefix from the response text if prefix is present', () => { xhrCreated.then((mock) => mock.respond(200, [], 'while(1){"a": 1}')); return xhr .fetchJson('/abc') .then((res) => xhr.xssiJson(res, 'while(1)')) .then((json) => { expect(json).to.be.deep.equal({a: 1}); }); }); }); });