UNPKG

chrome-devtools-frontend

Version:
551 lines (480 loc) • 23.3 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import {expectCookie} from '../../testing/Cookies.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import { describeWithMockConnection, setMockConnectionResponseHandler, } from '../../testing/MockConnection.js'; import * as Platform from '../platform/platform.js'; import * as SDK from './sdk.js'; const {urlString} = Platform.DevToolsPath; describe('NetworkRequest', () => { it('can parse statusText from the first line of responseReceivedExtraInfo\'s headersText', () => { assert.strictEqual( SDK.NetworkRequest.NetworkRequest.parseStatusTextFromResponseHeadersText('HTTP/1.1 304 not modified'), 'not modified'); assert.strictEqual( SDK.NetworkRequest.NetworkRequest.parseStatusTextFromResponseHeadersText('HTTP/1.1 200 OK'), 'OK'); assert.strictEqual( SDK.NetworkRequest.NetworkRequest.parseStatusTextFromResponseHeadersText('HTTP/1.1 200 OK\r\n\r\nfoo: bar\r\n'), 'OK'); }); it('parses response cookies from headers', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); request.addExtraResponseInfo({ blockedResponseCookies: [], responseHeaders: [{name: 'Set-Cookie', value: 'foo=bar'}, {name: 'Set-Cookie', value: 'baz=qux'}], resourceIPAddressSpace: 'Public' as Protocol.Network.IPAddressSpace, } as unknown as SDK.NetworkRequest.ExtraResponseInfo); assert.lengthOf(request.responseCookies, 2); expectCookie(request.responseCookies[0], {name: 'foo', value: 'bar', size: 8}); expectCookie(request.responseCookies[1], {name: 'baz', value: 'qux', size: 7}); }); it('infers status text from status code if none given', () => { const fakeRequest = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'fakeRequestId', urlString`url1`, urlString`documentURL`, null, ); fakeRequest.statusCode = 200; assert.strictEqual(fakeRequest.statusText, ''); assert.strictEqual(fakeRequest.getInferredStatusText(), 'OK'); }); it('does not infer status text from unknown status code', () => { const fakeRequest = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'fakeRequestId', urlString`url1`, urlString`documentURL`, null, ); fakeRequest.statusCode = 999; assert.strictEqual(fakeRequest.statusText, ''); assert.strictEqual(fakeRequest.getInferredStatusText(), ''); }); it('infers status text only when no status text given', () => { const fakeRequest = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'fakeRequestId', urlString`url1`, urlString`documentURL`, null, ); fakeRequest.statusCode = 200; fakeRequest.statusText = 'Prefer me'; assert.strictEqual(fakeRequest.statusText, 'Prefer me'); assert.strictEqual(fakeRequest.getInferredStatusText(), 'Prefer me'); }); it('includes partition key in response cookies', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); request.addExtraResponseInfo({ blockedResponseCookies: [], responseHeaders: [{name: 'Set-Cookie', value: 'foo=bar'}, {name: 'Set-Cookie', value: 'baz=qux; Secure;Partitioned'}], resourceIPAddressSpace: 'Public' as Protocol.Network.IPAddressSpace, cookiePartitionKey: {topLevelSite: 'partitionKey', hasCrossSiteAncestor: false}, } as unknown as SDK.NetworkRequest.ExtraResponseInfo); assert.lengthOf(request.responseCookies, 2); expectCookie(request.responseCookies[0], {name: 'foo', value: 'bar', size: 8}); expectCookie(request.responseCookies[1], { name: 'baz', value: 'qux', secure: true, partitionKey: {topLevelSite: 'partitionKey', hasCrossSiteAncestor: false}, size: 27, }); }); it('determines whether the response headers have been overridden', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); request.responseHeaders = [{name: 'foo', value: 'bar'}]; request.originalResponseHeaders = [{name: 'foo', value: 'baz'}]; assert.isTrue(request.hasOverriddenHeaders()); request.originalResponseHeaders = []; assert.isFalse(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'Foo', value: 'bar'}]; assert.isFalse(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'Foo', value: 'Bar'}]; assert.isTrue(request.hasOverriddenHeaders()); request.responseHeaders = [{name: 'one', value: 'first'}, {name: 'two', value: 'second'}]; request.originalResponseHeaders = [{name: 'ONE', value: 'first'}, {name: 'Two', value: 'second'}]; assert.isFalse(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'one', value: 'first'}]; assert.isTrue(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'two', value: 'second'}, {name: 'one', value: 'first'}]; assert.isFalse(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'one', value: 'second'}, {name: 'two', value: 'first'}]; assert.isTrue(request.hasOverriddenHeaders()); request.originalResponseHeaders = [{name: 'one', value: 'first'}, {name: 'two', value: 'second'}, {name: 'two', value: 'second'}]; assert.isTrue(request.hasOverriddenHeaders()); }); it('considers duplicate headers which only differ in the order of their values as overridden', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); request.responseHeaders = [{name: 'duplicate', value: 'first'}, {name: 'duplicate', value: 'second'}]; request.originalResponseHeaders = [{name: 'duplicate', value: 'second'}, {name: 'duplicate', value: 'first'}]; assert.isTrue(request.hasOverriddenHeaders()); }); it('can handle the case of duplicate cookies with only 1 of them being blocked', async () => { const request = SDK.NetworkRequest.NetworkRequest.create( 'requestId' as Protocol.Network.RequestId, urlString`url`, urlString`documentURL`, null, null, null); request.addExtraResponseInfo({ responseHeaders: [{name: 'Set-Cookie', value: 'foo=duplicate; Path=/\nfoo=duplicate; Path=/'}], blockedResponseCookies: [{ blockedReasons: [Protocol.Network.SetCookieBlockedReason.SameSiteNoneInsecure], cookie: null, cookieLine: 'foo=duplicate; Path=/', }], resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public, statusCode: undefined, cookiePartitionKey: undefined, cookiePartitionKeyOpaque: undefined, exemptedResponseCookies: undefined, }); assert.deepEqual( request.responseCookies.map(cookie => cookie.getCookieLine()), ['foo=duplicate; Path=/', 'foo=duplicate; Path=/']); assert.deepEqual(request.blockedResponseCookies(), [{ blockedReasons: [Protocol.Network.SetCookieBlockedReason.SameSiteNoneInsecure], cookie: null, cookieLine: 'foo=duplicate; Path=/', }]); assert.deepEqual( request.nonBlockedResponseCookies().map(cookie => cookie.getCookieLine()), ['foo=duplicate; Path=/']); }); it('can handle the case of exempted cookies', async () => { const request = SDK.NetworkRequest.NetworkRequest.create( 'requestId' as Protocol.Network.RequestId, urlString`url`, urlString`documentURL`, null, null, null); const cookie = new SDK.Cookie.Cookie('name', 'value'); cookie.addAttribute(SDK.Cookie.Attribute.SAME_SITE, 'None'); cookie.addAttribute(SDK.Cookie.Attribute.SECURE, true); cookie.setCookieLine('name=value; Path=/; SameSite=None; Secure;'); request.addExtraResponseInfo({ responseHeaders: [{name: 'Set-Cookie', value: cookie.getCookieLine() as string}], blockedResponseCookies: [], resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public, statusCode: undefined, cookiePartitionKey: undefined, cookiePartitionKeyOpaque: undefined, exemptedResponseCookies: [{ cookie, cookieLine: cookie.getCookieLine() as string, exemptionReason: Protocol.Network.CookieExemptionReason.TPCDHeuristics, }], }); assert.deepEqual( request.responseCookies.map(cookie => cookie.getCookieLine()), ['name=value; Path=/; SameSite=None; Secure;']); assert.deepEqual( request.nonBlockedResponseCookies().map(cookie => cookie.getCookieLine()), ['name=value; Path=/; SameSite=None; Secure;']); assert.deepEqual( request.exemptedResponseCookies().map(cookie => cookie.cookie.getCookieLine()), ['name=value; Path=/; SameSite=None; Secure;']); assert.deepEqual( request.exemptedResponseCookies().map(cookie => cookie.exemptionReason), [Protocol.Network.CookieExemptionReason.TPCDHeuristics]); request.addExtraRequestInfo({ blockedRequestCookies: [], requestHeaders: [], includedRequestCookies: [{exemptionReason: Protocol.Network.CookieExemptionReason.EnterprisePolicy, cookie}], connectTiming: {requestTime: 0}, }); assert.deepEqual( request.includedRequestCookies().map(included => included.cookie.getCookieLine()), ['name=value; Path=/; SameSite=None; Secure;']); assert.deepEqual( request.includedRequestCookies().map(included => included.exemptionReason), [Protocol.Network.CookieExemptionReason.EnterprisePolicy]); }); it('preserves order of headers in case of duplicates', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); const responseHeaders = [{name: '1ab', value: 'middle'}, {name: '1aB', value: 'last'}]; request.addExtraResponseInfo({ blockedResponseCookies: [], responseHeaders, resourceIPAddressSpace: 'Public' as Protocol.Network.IPAddressSpace, } as unknown as SDK.NetworkRequest.ExtraResponseInfo); assert.deepEqual(request.sortedResponseHeaders, responseHeaders); }); it('treats multiple headers with the same name the same as single header with comma-separated values', () => { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'requestId', urlString`url`, urlString`documentURL`, null); request.responseHeaders = [{name: 'duplicate', value: 'first, second'}]; request.originalResponseHeaders = [{name: 'duplicate', value: 'first'}, {name: 'duplicate', value: 'second'}]; assert.isFalse(request.hasOverriddenHeaders()); }); }); describeWithMockConnection('NetworkRequest', () => { let networkManagerForRequestStub: sinon.SinonStub; let cookie: SDK.Cookie.Cookie; let addBlockedCookieSpy: sinon.SinonSpy; let target: SDK.Target.Target; beforeEach(() => { target = createTarget(); const networkManager = target.model(SDK.NetworkManager.NetworkManager); assert.exists(networkManager); networkManagerForRequestStub = sinon.stub(SDK.NetworkManager.NetworkManager, 'forRequest').returns(networkManager); cookie = new SDK.Cookie.Cookie('name', 'value'); addBlockedCookieSpy = sinon.spy(SDK.CookieModel.CookieModel.prototype, 'addBlockedCookie'); }); afterEach(() => { networkManagerForRequestStub.restore(); }); it('adds blocked response cookies to - and removes exempted cookies from cookieModel', async () => { const removeBlockedCookieSpy = sinon.spy(SDK.CookieModel.CookieModel.prototype, 'removeBlockedCookie'); setMockConnectionResponseHandler('Network.getCookies', () => ({cookies: []})); const cookieModel = target.model(SDK.CookieModel.CookieModel); assert.exists(cookieModel); const url = urlString`url`; const request = SDK.NetworkRequest.NetworkRequest.create( 'requestId' as Protocol.Network.RequestId, url, urlString`documentURL`, null, null, null); request.addExtraResponseInfo({ responseHeaders: [{name: 'Set-Cookie', value: 'name=value; Path=/'}], blockedResponseCookies: [{ blockedReasons: [Protocol.Network.SetCookieBlockedReason.ThirdPartyPhaseout], cookie, cookieLine: 'name=value; Path=/', }], resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public, statusCode: undefined, cookiePartitionKey: undefined, cookiePartitionKeyOpaque: undefined, exemptedResponseCookies: undefined, }); assert.isTrue(addBlockedCookieSpy.calledOnceWith(cookie, [ { attribute: null, uiString: 'Setting this cookie was blocked either because of Chrome flags or browser configuration. Learn more in the Issues panel.', }, ])); assert.deepEqual(await cookieModel.getCookiesForDomain(''), [cookie]); request.addExtraResponseInfo({ responseHeaders: [{name: 'Set-Cookie', value: 'name=value; Path=/'}], blockedResponseCookies: [], resourceIPAddressSpace: Protocol.Network.IPAddressSpace.Public, statusCode: undefined, cookiePartitionKey: undefined, cookiePartitionKeyOpaque: undefined, exemptedResponseCookies: [{ cookie, cookieLine: cookie.getCookieLine() as string, exemptionReason: Protocol.Network.CookieExemptionReason.TPCDHeuristics, }], }); assert.isTrue(removeBlockedCookieSpy.calledOnceWith(cookie)); assert.isEmpty(await cookieModel.getCookiesForDomain('')); }); }); describeWithMockConnection('ServerSentEvents', () => { let target: SDK.Target.Target; let networkManager: SDK.NetworkManager.NetworkManager; beforeEach(() => { target = createTarget(); networkManager = target.model(SDK.NetworkManager.NetworkManager) as SDK.NetworkManager.NetworkManager; }); it('sends EventSourceMessageAdded events for EventSource text/event-stream', () => { networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/sse', }, type: 'EventSource', } as Protocol.Network.RequestWillBeSentEvent); networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/sse', mimeType: 'text/event-stream', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); const networkEvents: SDK.NetworkRequest.EventSourceMessage[] = []; networkManager.requestForId('1')!.addEventListener( SDK.NetworkRequest.Events.EVENT_SOURCE_MESSAGE_ADDED, ({data}) => networkEvents.push(data)); networkManager.dispatcher.eventSourceMessageReceived({ requestId: '1' as Protocol.Network.RequestId, timestamp: 21, data: 'foo', eventId: 'fooId', eventName: 'fooName', }); networkManager.dispatcher.eventSourceMessageReceived({ requestId: '1' as Protocol.Network.RequestId, timestamp: 42, data: 'bar', eventId: 'barId', eventName: 'barName', }); assert.lengthOf(networkEvents, 2); assert.deepEqual(networkEvents[0], {data: 'foo', eventId: 'fooId', eventName: 'fooName', time: 21}); assert.deepEqual(networkEvents[1], {data: 'bar', eventId: 'barId', eventName: 'barName', time: 42}); }); it('sends EventSourceMessageAdded events for raw text/event-stream', async () => { setMockConnectionResponseHandler('Network.streamResourceContent', () => ({ getError() { return undefined; }, bufferedData: '', })); networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/sse', }, type: 'Fetch', } as Protocol.Network.RequestWillBeSentEvent); networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/sse', mimeType: 'text/event-stream', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); const networkEvents: SDK.NetworkRequest.EventSourceMessage[] = []; const {promise: twoEventsReceivedPromise, resolve} = Promise.withResolvers<void>(); networkManager.requestForId('1')!.addEventListener( SDK.NetworkRequest.Events.EVENT_SOURCE_MESSAGE_ADDED, ({data}) => { networkEvents.push(data); if (networkEvents.length === 2) { resolve(); } }); const message = ` id: fooId event: fooName data: foo id: barId event: barName data: bar\n\n`; // Send `message` piecemeal via dataReceived events. let time = 0; for (const c of message) { networkManager.dispatcher.dataReceived({ requestId: '1' as Protocol.Network.RequestId, dataLength: 1, encodedDataLength: 1, timestamp: time++, data: window.btoa(c), }); } await twoEventsReceivedPromise; // Omit time from expectation as the dataReceived loop is racing against the text decoder. assert.lengthOf(networkEvents, 2); assert.deepInclude(networkEvents[0], {data: 'foo', eventId: 'fooId', eventName: 'fooName'}); assert.deepInclude(networkEvents[1], {data: 'bar', eventId: 'barId', eventName: 'barName'}); }); }); describeWithMockConnection('requestStreamingContent', () => { let target: SDK.Target.Target; let networkManager: SDK.NetworkManager.NetworkManager; beforeEach(() => { target = createTarget(); networkManager = target.model(SDK.NetworkManager.NetworkManager) as SDK.NetworkManager.NetworkManager; }); it('retrieves the full response body for finished requests', () => { networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/index.html', }, type: 'Document', } as Protocol.Network.RequestWillBeSentEvent); networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/index.html', mimeType: 'text/html', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); networkManager.dispatcher.loadingFinished({ requestId: '1' as Protocol.Network.RequestId, } as Protocol.Network.LoadingFinishedEvent); const responseBodySpy = sinon.spy(target.networkAgent(), 'invoke_getResponseBody'); void networkManager.requestForId('1')!.requestStreamingContent(); assert.isTrue(responseBodySpy.calledOnce); }); it('streams the full response body for in-flight requests', () => { networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/index.html', }, type: 'Document', } as Protocol.Network.RequestWillBeSentEvent); networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/index.html', mimeType: 'text/html', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); const responseBodySpy = sinon.spy(target.networkAgent(), 'invoke_streamResourceContent'); void networkManager.requestForId('1')!.requestStreamingContent(); assert.isTrue(responseBodySpy.calledOnce); }); it('sends ChunkAdded events when new data is received', async () => { networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/index.html', }, type: 'Document', } as Protocol.Network.RequestWillBeSentEvent); networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/index.html', mimeType: 'text/html', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); sinon.stub(SDK.NetworkManager.NetworkManager, 'streamResponseBody') .returns(Promise.resolve(new TextUtils.ContentData.ContentData('Zm9v', true, 'text/html'))); const maybeStreamingContent = await networkManager.requestForId('1')!.requestStreamingContent(); assert.isFalse(TextUtils.StreamingContentData.isError(maybeStreamingContent)); const streamingContent = maybeStreamingContent as TextUtils.StreamingContentData.StreamingContentData; const eventPromise = streamingContent.once(TextUtils.StreamingContentData.Events.CHUNK_ADDED); networkManager.dispatcher.dataReceived({ requestId: '1' as Protocol.Network.RequestId, data: 'YmFy', dataLength: 4, encodedDataLength: 4, timestamp: 42, }); const {chunk} = await eventPromise; assert.strictEqual(chunk, 'YmFy'); assert.strictEqual(streamingContent.content().text, 'foobar'); }); it('waits for "responseReceived" event to construct the StreamingContentData', async () => { networkManager.dispatcher.requestWillBeSent({ requestId: '1' as Protocol.Network.RequestId, request: { url: 'https://example.com/index.html', }, type: 'Document', } as Protocol.Network.RequestWillBeSentEvent); sinon.stub(target.networkAgent(), 'invoke_streamResourceContent') .returns(Promise.resolve({bufferedData: '', getError: () => undefined})); const streamingContentDataPromise = networkManager.requestForId('1')!.requestStreamingContent(); // Trigger the "responseReceived" on the next event loop tick. setTimeout(() => { networkManager.dispatcher.responseReceived({ requestId: '1' as Protocol.Network.RequestId, response: { url: 'https://example.com/index.html', mimeType: 'text/html', } as Protocol.Network.Response, } as Protocol.Network.ResponseReceivedEvent); }, 0); const maybeStreamingContent = await streamingContentDataPromise; assert.isFalse(TextUtils.StreamingContentData.isError(maybeStreamingContent)); const streamingContent = maybeStreamingContent as TextUtils.StreamingContentData.StreamingContentData; assert.strictEqual(streamingContent.mimeType, 'text/html'); }); });