| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../../core/common/common.js'; |
| import * as Host from '../../../core/host/host.js'; |
| import * as Platform from '../../../core/platform/platform.js'; |
| import * as SDK from '../../../core/sdk/sdk.js'; |
| import * as Protocol from '../../../generated/protocol.js'; |
| import type * as Persistence from '../../../models/persistence/persistence.js'; |
| import * as Workspace from '../../../models/workspace/workspace.js'; |
| import { |
| dispatchInputEvent, |
| getCleanTextContentFromElements, |
| renderElementIntoDOM, |
| } from '../../../testing/DOMHelpers.js'; |
| import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js'; |
| import { |
| createWorkspaceProject, |
| setUpEnvironment, |
| } from '../../../testing/OverridesHelpers.js'; |
| import { |
| recordedMetricsContain, |
| resetRecordedMetrics, |
| } from '../../../testing/UserMetricsHelpers.js'; |
| import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js'; |
| import * as NetworkForward from '../forward/forward.js'; |
| |
| import * as NetworkComponents from './components.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| const enum HeaderAttribute { |
| HEADER_NAME = 'HeaderName', |
| HEADER_VALUE = 'HeaderValue', |
| } |
| |
| async function renderResponseHeaderSection(request: SDK.NetworkRequest.NetworkRequest): |
| Promise<NetworkComponents.ResponseHeaderSection.ResponseHeaderSection> { |
| const component = new NetworkComponents.ResponseHeaderSection.ResponseHeaderSection(); |
| renderElementIntoDOM(component); |
| Object.setPrototypeOf(request, SDK.NetworkRequest.NetworkRequest.prototype); |
| component.data = { |
| request, |
| toReveal: {section: NetworkForward.UIRequestLocation.UIHeaderSection.RESPONSE, header: 'highlighted-header'}, |
| }; |
| await RenderCoordinator.done(); |
| assert.instanceOf(component, HTMLElement); |
| assert.isNotNull(component.shadowRoot); |
| return component; |
| } |
| |
| async function editHeaderRow( |
| component: NetworkComponents.ResponseHeaderSection.ResponseHeaderSection, index: number, |
| headerAttribute: HeaderAttribute, newValue: string) { |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.isTrue(rows.length >= index + 1, 'Trying to edit row with index greater than # of rows.'); |
| const row = rows[index]; |
| assert.isNotNull(row.shadowRoot); |
| const selector = headerAttribute === HeaderAttribute.HEADER_NAME ? '.header-name' : '.header-value'; |
| const editableComponent = row.shadowRoot.querySelector(`${selector} devtools-editable-span`); |
| assert.instanceOf(editableComponent, HTMLElement); |
| assert.isNotNull(editableComponent.shadowRoot); |
| const editable = editableComponent.shadowRoot.querySelector('.editable'); |
| assert.instanceOf(editable, HTMLSpanElement); |
| editable.focus(); |
| editable.innerText = newValue; |
| dispatchInputEvent(editable, {inputType: 'insertText', data: newValue, bubbles: true, composed: true}); |
| editable.blur(); |
| await RenderCoordinator.done(); |
| } |
| |
| async function removeHeaderRow( |
| component: NetworkComponents.ResponseHeaderSection.ResponseHeaderSection, index: number): Promise<void> { |
| assert.isNotNull(component.shadowRoot); |
| const row = component.shadowRoot.querySelectorAll('devtools-header-section-row')[index]; |
| assert.instanceOf(row, HTMLElement); |
| assert.isNotNull(row.shadowRoot); |
| const button = row.shadowRoot.querySelector('.remove-header'); |
| assert.instanceOf(button, HTMLElement); |
| button.click(); |
| await RenderCoordinator.done(); |
| } |
| |
| async function setupHeaderEditing( |
| headerOverridesFileContent: string, actualHeaders: SDK.NetworkRequest.NameValue[], |
| originalHeaders: SDK.NetworkRequest.NameValue[]) { |
| const request = { |
| sortedResponseHeaders: actualHeaders, |
| originalResponseHeaders: originalHeaders, |
| setCookieHeaders: [], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => false, |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| return await setupHeaderEditingWithRequest(headerOverridesFileContent, request); |
| } |
| |
| async function setupHeaderEditingWithRequest( |
| headerOverridesFileContent: string, request: SDK.NetworkRequest.NetworkRequest) { |
| const networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, [ |
| { |
| name: '.headers', |
| path: 'www.example.com/', |
| content: headerOverridesFileContent, |
| }, |
| ]); |
| |
| const project = networkPersistenceManager.project(); |
| let spy = sinon.spy(); |
| if (project) { |
| const uiSourceCode = project.uiSourceCodeForURL(urlString`file:///path/to/overrides/www.example.com/.headers`); |
| if (uiSourceCode) { |
| spy = sinon.spy(uiSourceCode, 'setWorkingCopy'); |
| } |
| } |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| return {component, spy}; |
| } |
| |
| function checkHeaderSectionRow( |
| row: NetworkComponents.HeaderSectionRow.HeaderSectionRow, headerName: string, headerValue: string, |
| isOverride: boolean, isNameEditable: boolean, isValueEditable: boolean, isHighlighted = false, |
| isDeleted = false): void { |
| assert.isNotNull(row.shadowRoot); |
| const rowElement = row.shadowRoot.querySelector('.row'); |
| assert.strictEqual(rowElement?.classList.contains('header-overridden'), isOverride); |
| assert.strictEqual(rowElement?.classList.contains('header-highlight'), isHighlighted); |
| assert.strictEqual(rowElement?.classList.contains('header-deleted'), isDeleted); |
| |
| const nameEditableComponent = |
| row.shadowRoot.querySelector<NetworkComponents.EditableSpan.EditableSpan>('.header-name devtools-editable-span'); |
| if (isNameEditable) { |
| assert.instanceOf(nameEditableComponent, HTMLElement); |
| assert.isNotNull(nameEditableComponent.shadowRoot); |
| const nameEditable = nameEditableComponent.shadowRoot.querySelector('.editable'); |
| assert.instanceOf(nameEditable, HTMLSpanElement); |
| const textContent = |
| nameEditable.textContent?.trim() + (row.shadowRoot.querySelector('.header-name')?.textContent || '').trim(); |
| assert.strictEqual(textContent, headerName); |
| } else { |
| assert.isNull(nameEditableComponent); |
| assert.strictEqual(row.shadowRoot.querySelector('.header-name')?.textContent?.trim(), headerName); |
| } |
| |
| const valueEditableComponent = |
| row.shadowRoot.querySelector<NetworkComponents.EditableSpan.EditableSpan>('.header-value devtools-editable-span'); |
| if (isValueEditable) { |
| assert.instanceOf(valueEditableComponent, HTMLElement); |
| assert.isNotNull(valueEditableComponent.shadowRoot); |
| const valueEditable = valueEditableComponent.shadowRoot.querySelector('.editable'); |
| assert.instanceOf(valueEditable, HTMLSpanElement); |
| assert.strictEqual(valueEditable.textContent?.trim(), headerValue); |
| } else { |
| assert.isNull(valueEditableComponent); |
| assert.strictEqual(row.shadowRoot.querySelector('.header-value')?.textContent?.trim(), headerValue); |
| } |
| } |
| |
| function isRowFocused( |
| component: NetworkComponents.ResponseHeaderSection.ResponseHeaderSection, rowIndex: number): boolean { |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.isTrue(rows.length > rowIndex); |
| return Boolean(rows[rowIndex].shadowRoot?.activeElement); |
| } |
| |
| describeWithEnvironment('ResponseHeaderSection', () => { |
| beforeEach(async () => { |
| await setUpEnvironment(); |
| resetRecordedMetrics(); |
| }); |
| |
| it('renders detailed reason for blocked requests', async () => { |
| const request = { |
| sortedResponseHeaders: [ |
| {name: 'content-length', value: '661'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => true, |
| blockedReason: () => Protocol.Network.BlockedReason.CorpNotSameOriginAfterDefaultedToSameOriginByCoep, |
| originalResponseHeaders: [], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| |
| const row = component.shadowRoot.querySelectorAll('devtools-header-section-row')[1]; |
| assert.instanceOf(row, HTMLElement); |
| assert.isNotNull(row.shadowRoot); |
| |
| const regex = /^\s*not-set\s*cross-origin-resource-policy\s*$/; |
| assert.isTrue(regex.test(row.shadowRoot.querySelector('.header-name')?.textContent || '')); |
| assert.strictEqual(row.shadowRoot.querySelector('.header-value')?.textContent?.trim(), ''); |
| assert.strictEqual( |
| getCleanTextContentFromElements(row.shadowRoot, '.call-to-action')[0], |
| `To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers: |
| Cross-Origin-Resource-Policy: same-site Choose this option if the resource and the document are served from the same site. |
| Cross-Origin-Resource-Policy: cross-origin Only choose this option if an arbitrary website including this resource does not impose a security risk. |
| Learn more`, |
| ); |
| }); |
| |
| it('displays info about blocked "Set-Cookie"-headers', async () => { |
| const request = { |
| sortedResponseHeaders: [{name: 'Set-Cookie', value: 'secure=only; Secure'}], |
| blockedResponseCookies: () => [{ |
| blockedReasons: ['SecureOnly', 'OverwriteSecure'], |
| cookieLine: 'secure=only; Secure', |
| cookie: null, |
| }], |
| wasBlocked: () => false, |
| originalResponseHeaders: [], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| |
| const row = component.shadowRoot.querySelector('devtools-header-section-row'); |
| assert.instanceOf(row, HTMLElement); |
| assert.isNotNull(row.shadowRoot); |
| |
| assert.strictEqual(row.shadowRoot.querySelector('.header-name')?.textContent?.trim(), 'set-cookie'); |
| assert.strictEqual(row.shadowRoot.querySelector('.header-value')?.textContent?.trim(), 'secure=only; Secure'); |
| |
| const icon = row.shadowRoot.querySelector('devtools-icon'); |
| assert.instanceOf(icon, HTMLElement); |
| assert.strictEqual( |
| icon.title, |
| 'This attempt to set a cookie via a Set-Cookie header was blocked because it had the ' + |
| '"Secure" attribute but was not received over a secure connection.\nThis attempt to ' + |
| 'set a cookie via a Set-Cookie header was blocked because it was not sent over a ' + |
| 'secure connection and would have overwritten a cookie with the Secure attribute.'); |
| }); |
| |
| it('marks overridden headers', async () => { |
| const request = { |
| sortedResponseHeaders: [ |
| // keep names in alphabetical order |
| {name: 'duplicate-both-no-mismatch', value: 'foo'}, |
| {name: 'duplicate-both-no-mismatch', value: 'bar'}, |
| {name: 'duplicate-both-with-mismatch', value: 'Chrome'}, |
| {name: 'duplicate-both-with-mismatch', value: 'DevTools'}, |
| {name: 'duplicate-different-order', value: 'aaa'}, |
| {name: 'duplicate-different-order', value: 'bbb'}, |
| {name: 'duplicate-in-actual-headers', value: 'first'}, |
| {name: 'duplicate-in-actual-headers', value: 'second'}, |
| {name: 'duplicate-in-original-headers', value: 'two'}, |
| {name: 'duplicate-single-line', value: 'first line, second line'}, |
| {name: 'is-in-original-headers', value: 'not an override'}, |
| {name: 'not-in-original-headers', value: 'is an override'}, |
| {name: 'triplicate', value: '1'}, |
| {name: 'triplicate', value: '2'}, |
| {name: 'triplicate', value: '2'}, |
| {name: 'xyz', value: 'contains \tab'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => false, |
| originalResponseHeaders: [ |
| // keep names in alphabetical order |
| {name: 'duplicate-both-no-mismatch', value: 'foo'}, |
| {name: 'duplicate-both-no-mismatch', value: 'bar'}, |
| {name: 'duplicate-both-with-mismatch', value: 'Chrome'}, |
| {name: 'duplicate-both-with-mismatch', value: 'Canary'}, |
| {name: 'duplicate-different-order', value: 'bbb'}, |
| {name: 'duplicate-different-order', value: 'aaa'}, |
| {name: 'duplicate-in-actual-headers', value: 'first'}, |
| {name: 'duplicate-in-original-headers', value: 'one'}, |
| {name: 'duplicate-in-original-headers', value: 'two'}, |
| {name: 'duplicate-single-line', value: 'first line'}, |
| {name: 'duplicate-single-line', value: 'second line'}, |
| {name: 'is-in-original-headers', value: 'not an override'}, |
| {name: 'triplicate', value: '1'}, |
| {name: 'triplicate', value: '1'}, |
| {name: 'triplicate', value: '2'}, |
| {name: 'xyz', value: 'contains \tab'}, |
| ], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| |
| const checkRow = (shadowRoot: ShadowRoot, headerName: string, headerValue: string, isOverride: boolean) => { |
| assert.strictEqual(shadowRoot.querySelector('.header-name')?.textContent?.trim(), headerName); |
| assert.strictEqual(shadowRoot.querySelector('.header-value')?.textContent?.trim(), headerValue); |
| assert.strictEqual(shadowRoot.querySelector('.row')?.classList.contains('header-overridden'), isOverride); |
| }; |
| |
| assert.isNotNull(rows[0].shadowRoot); |
| checkRow(rows[0].shadowRoot, 'duplicate-both-no-mismatch', 'foo', false); |
| assert.isNotNull(rows[1].shadowRoot); |
| checkRow(rows[1].shadowRoot, 'duplicate-both-no-mismatch', 'bar', false); |
| assert.isNotNull(rows[2].shadowRoot); |
| checkRow(rows[2].shadowRoot, 'duplicate-both-with-mismatch', 'Chrome', true); |
| assert.isNotNull(rows[3].shadowRoot); |
| checkRow(rows[3].shadowRoot, 'duplicate-both-with-mismatch', 'DevTools', true); |
| assert.isNotNull(rows[4].shadowRoot); |
| checkRow(rows[4].shadowRoot, 'duplicate-different-order', 'aaa', true); |
| assert.isNotNull(rows[5].shadowRoot); |
| checkRow(rows[5].shadowRoot, 'duplicate-different-order', 'bbb', true); |
| assert.isNotNull(rows[6].shadowRoot); |
| checkRow(rows[6].shadowRoot, 'duplicate-in-actual-headers', 'first', true); |
| assert.isNotNull(rows[7].shadowRoot); |
| checkRow(rows[7].shadowRoot, 'duplicate-in-actual-headers', 'second', true); |
| assert.isNotNull(rows[8].shadowRoot); |
| checkRow(rows[8].shadowRoot, 'duplicate-in-original-headers', 'two', true); |
| assert.isNotNull(rows[9].shadowRoot); |
| checkRow(rows[9].shadowRoot, 'duplicate-single-line', 'first line, second line', false); |
| assert.isNotNull(rows[10].shadowRoot); |
| checkRow(rows[10].shadowRoot, 'is-in-original-headers', 'not an override', false); |
| assert.isNotNull(rows[11].shadowRoot); |
| checkRow(rows[11].shadowRoot, 'not-in-original-headers', 'is an override', true); |
| assert.isNotNull(rows[12].shadowRoot); |
| checkRow(rows[12].shadowRoot, 'triplicate', '1', true); |
| assert.isNotNull(rows[13].shadowRoot); |
| checkRow(rows[13].shadowRoot, 'triplicate', '2', true); |
| assert.isNotNull(rows[14].shadowRoot); |
| checkRow(rows[14].shadowRoot, 'triplicate', '2', true); |
| assert.isNotNull(rows[15].shadowRoot); |
| checkRow(rows[15].shadowRoot, 'xyz', 'contains ab', false); |
| }); |
| |
| it('correctly sets headers as "editable" when matching ".headers" file exists and setting is turned on', async () => { |
| await createWorkspaceProject(urlString`file:///path/to/overrides`, [ |
| { |
| name: '.headers', |
| path: 'www.example.com/', |
| content: `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`, |
| }, |
| ]); |
| |
| const request = { |
| sortedResponseHeaders: [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'overridden server'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => false, |
| originalResponseHeaders: [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'original server'}, |
| ], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| |
| checkHeaderSectionRow(rows[0], 'cache-control', 'max-age=600', false, false, true); |
| checkHeaderSectionRow(rows[1], 'server', 'overridden server', true, false, true); |
| |
| Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(false); |
| component.data = {request}; |
| await RenderCoordinator.done(); |
| |
| checkHeaderSectionRow(rows[0], 'cache-control', 'max-age=600', false, false, false); |
| checkHeaderSectionRow(rows[1], 'server', 'overridden server', true, false, false); |
| |
| Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(true); |
| }); |
| |
| it('does not set headers as "editable" when matching ".headers" file cannot be parsed correctly', async () => { |
| await createWorkspaceProject(urlString`file:///path/to/overrides`, [ |
| { |
| name: '.headers', |
| path: 'www.example.com/', |
| // 'headers' contains the invalid key 'no-name' and will therefore |
| // cause a parsing error. |
| content: `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "no-name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`, |
| }, |
| ]); |
| |
| const request = { |
| sortedResponseHeaders: [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'overridden server'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => false, |
| originalResponseHeaders: [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'original server'}, |
| ], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| // A console error is emitted when '.headers' cannot be parsed correctly. |
| // We don't need that noise in the test output. |
| sinon.stub(console, 'error'); |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| |
| assert.isNotNull(rows[0].shadowRoot); |
| checkHeaderSectionRow(rows[0], 'cache-control', 'max-age=600', false, false, false); |
| assert.isNotNull(rows[1].shadowRoot); |
| checkHeaderSectionRow(rows[1], 'server', 'overridden server', true, false, false); |
| }); |
| |
| it('can edit original headers', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'overridden server'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'original server'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'max-age=9999'); |
| |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'overridden server', |
| }, |
| { |
| name: 'cache-control', |
| value: 'max-age=9999', |
| }, |
| ], |
| }]; |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| assert.isTrue(recordedMetricsContain( |
| Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, |
| Host.UserMetrics.Action.HeaderOverrideHeaderEdited)); |
| }); |
| |
| it('can handle tab-character in header value', async () => { |
| const headers = [ |
| {name: 'foo', value: 'syn\tax'}, |
| ]; |
| const {component, spy} = await setupHeaderEditing('[]', headers, headers); |
| assert.isNotNull(component.shadowRoot); |
| |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 1); |
| checkHeaderSectionRow(rows[0], 'foo', 'syn ax', false, false, true); |
| |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'syn ax'); |
| sinon.assert.notCalled(spy); |
| checkHeaderSectionRow(rows[0], 'foo', 'syn ax', false, false, true); |
| |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'syntax'); |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'foo', |
| value: 'syntax', |
| }, |
| ], |
| }]; |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| checkHeaderSectionRow(rows[0], 'foo', 'syntax', true, false, true); |
| }); |
| |
| it('can edit overridden headers', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'overridden server'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'original server'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'edited value'); |
| |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'edited value', |
| }, |
| ], |
| }]; |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| assert.isTrue(recordedMetricsContain( |
| Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, |
| Host.UserMetrics.Action.HeaderOverrideHeaderEdited)); |
| }); |
| |
| it('can remove header overrides', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [ |
| { |
| "name": "highlighted-header", |
| "value": "overridden highlighted-header" |
| }, |
| { |
| "name": "cache-control", |
| "value": "max-age=9999" |
| }, |
| { |
| "name": "added", |
| "value": "foo" |
| } |
| ] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'added', value: 'foo'}, |
| {name: 'cache-control', value: 'max-age=9999'}, |
| {name: 'highlighted-header', value: 'overridden highlighted-header'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'highlighted-header', value: 'original highlighted-header'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| assert.isNotNull(component.shadowRoot); |
| let rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 3); |
| checkHeaderSectionRow(rows[0], 'added', 'foo', true, false, true); |
| checkHeaderSectionRow(rows[1], 'cache-control', 'max-age=9999', true, false, true); |
| checkHeaderSectionRow(rows[2], 'highlighted-header', 'overridden highlighted-header', true, false, true, true); |
| await removeHeaderRow(component, 2); |
| |
| let expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'cache-control', |
| value: 'max-age=9999', |
| }, |
| { |
| name: 'added', |
| value: 'foo', |
| }, |
| ], |
| }]; |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| assert.isTrue(recordedMetricsContain( |
| Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, |
| Host.UserMetrics.Action.HeaderOverrideHeaderRemoved)); |
| |
| rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 3); |
| checkHeaderSectionRow(rows[0], 'added', 'foo', true, false, true); |
| checkHeaderSectionRow(rows[1], 'cache-control', 'max-age=9999', true, false, true); |
| checkHeaderSectionRow( |
| rows[2], 'highlighted-header', 'overridden highlighted-header', true, false, false, true, true); |
| |
| spy.resetHistory(); |
| await removeHeaderRow(component, 0); |
| |
| expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'cache-control', |
| value: 'max-age=9999', |
| }, |
| ], |
| }]; |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 3); |
| checkHeaderSectionRow(rows[0], 'added', 'foo', true, false, false, false, true); |
| checkHeaderSectionRow(rows[1], 'cache-control', 'max-age=9999', true, false, true); |
| checkHeaderSectionRow( |
| rows[2], 'highlighted-header', 'overridden highlighted-header', true, false, false, true, true); |
| }); |
| |
| it('can remove the last header override', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [ |
| { |
| "name": "server", |
| "value": "overridden server" |
| } |
| ] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'server', value: 'overridden server'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'server', value: 'original server'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await removeHeaderRow(component, 0); |
| |
| const expected: Persistence.NetworkPersistenceManager.HeaderOverride[] = []; |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| assert.isTrue(recordedMetricsContain( |
| Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, |
| Host.UserMetrics.Action.HeaderOverrideHeaderRemoved)); |
| }); |
| |
| it('can handle non-breaking spaces when removing header overrides', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [ |
| { |
| "name": "added", |
| "value": "space\xa0between" |
| } |
| ] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'added', value: 'space between'}, |
| {name: 'cache-control', value: 'max-age=600'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| assert.isNotNull(component.shadowRoot); |
| let rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'added', 'space between', true, false, true); |
| checkHeaderSectionRow(rows[1], 'cache-control', 'max-age=600', false, false, true); |
| await removeHeaderRow(component, 0); |
| |
| const expected: Persistence.NetworkPersistenceManager.HeaderOverride[] = []; |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| |
| rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'added', 'space between', true, false, false, false, true); |
| checkHeaderSectionRow(rows[1], 'cache-control', 'max-age=600', false, false, true); |
| }); |
| |
| it('does not generate header overrides which have "applyTo" but empty "headers" array', async () => { |
| const actualHeaders = [ |
| {name: 'server', value: 'original server'}, |
| ]; |
| const {component, spy} = await setupHeaderEditing('[]', actualHeaders, actualHeaders); |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'overridden server'); |
| |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'overridden server', |
| }, |
| ], |
| }]; |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify(expected, null, 2))); |
| |
| spy.resetHistory(); |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'original server'); |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify([], null, 2))); |
| }); |
| |
| it('can add headers', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| const actualHeaders = [{name: 'server', value: 'overridden server'}]; |
| const originalHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| let expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'overridden server', |
| }, |
| { |
| name: 'header-name', |
| value: 'header value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| |
| assert.isTrue(recordedMetricsContain( |
| Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, |
| Host.UserMetrics.Action.HeaderOverrideHeaderAdded)); |
| |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_NAME, 'foo'); |
| expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'overridden server', |
| }, |
| { |
| name: 'foo', |
| value: 'header value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'bar'); |
| expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'overridden server', |
| }, |
| { |
| name: 'foo', |
| value: 'bar', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| }); |
| |
| it('does not persist invalid header names', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| const actualHeaders = [{name: 'server', value: 'overridden server'}]; |
| const originalHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| sinon.assert.callCount(spy, 1); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_NAME, 'valid'); |
| sinon.assert.callCount(spy, 2); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_NAME, 'in:valid'); |
| sinon.assert.callCount(spy, 2); |
| }); |
| |
| it('can remove a newly added header', async () => { |
| const actualHeaders = [ |
| {name: 'server', value: 'original server'}, |
| ]; |
| const {component, spy} = await setupHeaderEditing('[]', actualHeaders, actualHeaders); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'header-name', |
| value: 'header value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| let rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'original server', false, false, true); |
| checkHeaderSectionRow(rows[1], 'header-name', 'header value', true, true, true); |
| |
| spy.resetHistory(); |
| await removeHeaderRow(component, 1); |
| |
| sinon.assert.callCount(spy, 1); |
| assert.isTrue(spy.calledOnceWith(JSON.stringify([], null, 2))); |
| rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'original server', false, false, true); |
| checkHeaderSectionRow(rows[1], 'header-name', 'header value', true, false, false, false, true); |
| }); |
| |
| it('renders headers as (not) editable depending on overall overrides setting', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/index.html`, urlString``, null, |
| null, null); |
| request.responseHeaders = [{name: 'server', value: 'overridden server'}]; |
| request.originalResponseHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const {component} = await setupHeaderEditingWithRequest('[]', request); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| let rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'overridden server', true, false, true); |
| checkHeaderSectionRow(rows[1], 'header-name', 'header value', true, true, true); |
| |
| component.remove(); |
| Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(false); |
| const component2 = await renderResponseHeaderSection(request); |
| assert.isNotNull(component2.shadowRoot); |
| |
| rows = component2.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'overridden server', true, false, false); |
| checkHeaderSectionRow(rows[1], 'header-name', 'header value', true, false, false); |
| |
| component2.remove(); |
| Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(true); |
| const component3 = await renderResponseHeaderSection(request); |
| assert.isNotNull(component3.shadowRoot); |
| |
| rows = component3.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'overridden server', true, false, true); |
| checkHeaderSectionRow(rows[1], 'header-name', 'header value', true, true, true); |
| }); |
| |
| it('can show the "edit header" button', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.foo.com/index.html`, urlString``, null, null, |
| null); |
| request.responseHeaders = [{name: 'foo', value: 'bar'}]; |
| request.originalResponseHeaders = [{name: 'foo', value: 'bar'}]; |
| |
| const {component} = await setupHeaderEditingWithRequest('[]', request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 1); |
| assert.isNotNull(rows[0].shadowRoot); |
| assert.isNotNull(rows[0].shadowRoot.querySelector('.enable-editing')); |
| }); |
| |
| it('does not show the "edit header" button for requests with a forbidden URL', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`chrome://terms/`, urlString``, null, null, null); |
| request.responseHeaders = [{name: 'foo', value: 'bar'}]; |
| request.originalResponseHeaders = [{name: 'foo', value: 'bar'}]; |
| |
| const {component} = await setupHeaderEditingWithRequest('[]', request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 1); |
| assert.isNotNull(rows[0].shadowRoot); |
| assert.isNull(rows[0].shadowRoot.querySelector('.enable-editing')); |
| }); |
| |
| it('can edit multiple headers', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'overridden server'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'cache-control', value: 'max-age=600'}, |
| {name: 'server', value: 'original server'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'edited cache-control'); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'edited server'); |
| |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'cache-control', |
| value: 'edited cache-control', |
| }, |
| { |
| name: 'server', |
| value: 'edited server', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.lastCall, JSON.stringify(expected, null, 2)); |
| }); |
| |
| it('can edit multiple headers which have the same name', async () => { |
| const headerOverridesFileContent = '[]'; |
| |
| const actualHeaders = [ |
| {name: 'link', value: 'first value'}, |
| {name: 'link', value: 'second value'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'link', value: 'first value'}, |
| {name: 'link', value: 'second value'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'third value'); |
| |
| let expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'link', |
| value: 'third value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.lastCall, JSON.stringify(expected, null, 2)); |
| |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'fourth value'); |
| expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'link', |
| value: 'third value', |
| }, |
| { |
| name: 'link', |
| value: 'fourth value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.lastCall, JSON.stringify(expected, null, 2)); |
| }); |
| |
| it('can edit multiple headers which have the same name and which are already overridden', async () => { |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [ |
| { |
| "name": "link", |
| "value": "third value" |
| }, |
| { |
| "name": "link", |
| "value": "fourth value" |
| } |
| ] |
| } |
| ]`; |
| |
| const actualHeaders = [ |
| {name: 'link', value: 'third value'}, |
| {name: 'link', value: 'fourth value'}, |
| ]; |
| |
| const originalHeaders = [ |
| {name: 'link', value: 'first value'}, |
| {name: 'link', value: 'second value'}, |
| ]; |
| |
| const {component, spy} = await setupHeaderEditing(headerOverridesFileContent, actualHeaders, originalHeaders); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'fifth value'); |
| |
| let expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'link', |
| value: 'third value', |
| }, |
| { |
| name: 'link', |
| value: 'fifth value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.lastCall, JSON.stringify(expected, null, 2)); |
| |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'sixth value'); |
| expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'link', |
| value: 'sixth value', |
| }, |
| { |
| name: 'link', |
| value: 'fifth value', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.lastCall, JSON.stringify(expected, null, 2)); |
| }); |
| |
| it('persists edits to header overrides and resurfaces them upon component (re-)creation', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/index.html`, urlString``, null, |
| null, null); |
| request.responseHeaders = [{name: 'server', value: 'overridden server'}]; |
| request.originalResponseHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const {component, spy} = await setupHeaderEditingWithRequest(headerOverridesFileContent, request); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'unit test'); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_NAME, 'foo'); |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'bar'); |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'server', |
| value: 'unit test', |
| }, |
| { |
| name: 'foo', |
| value: 'bar', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| |
| component.remove(); |
| const component2 = await renderResponseHeaderSection(request); |
| assert.isNotNull(component2.shadowRoot); |
| |
| const rows = component2.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 2); |
| checkHeaderSectionRow(rows[0], 'server', 'unit test', true, false, true); |
| checkHeaderSectionRow(rows[1], 'foo', 'bar', true, true, true); |
| }); |
| |
| it('focuses on newly added header rows on initial render', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/index.html`, urlString``, null, |
| null, null); |
| request.responseHeaders = [{name: 'server', value: 'overridden server'}]; |
| request.originalResponseHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const {component} = await setupHeaderEditingWithRequest(headerOverridesFileContent, request); |
| assert.isNotNull(component.shadowRoot); |
| const addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| assert.isFalse(isRowFocused(component, 0)); |
| assert.isTrue(isRowFocused(component, 1)); |
| |
| component.remove(); |
| const component2 = await renderResponseHeaderSection(request); |
| assert.isNotNull(component2.shadowRoot); |
| assert.isFalse(isRowFocused(component2, 0)); |
| assert.isFalse(isRowFocused(component2, 1)); |
| }); |
| |
| it('can handle removal of ".headers" file', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/index.html`, urlString``, null, |
| null, null); |
| request.responseHeaders = [{name: 'server', value: 'overridden server'}]; |
| request.originalResponseHeaders = [{name: 'server', value: 'original server'}]; |
| |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [{ |
| "name": "server", |
| "value": "overridden server" |
| }] |
| } |
| ]`; |
| |
| const {component} = await setupHeaderEditingWithRequest(headerOverridesFileContent, request); |
| assert.isNotNull(component.shadowRoot); |
| let addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.instanceOf(addHeaderButton, HTMLElement); |
| addHeaderButton.click(); |
| await RenderCoordinator.done(); |
| |
| await editHeaderRow(component, 0, HeaderAttribute.HEADER_VALUE, 'unit test'); |
| |
| sinon.stub(Workspace.Workspace.WorkspaceImpl.instance(), 'uiSourceCodeForURL').callsFake(() => null); |
| |
| component.data = {request}; |
| await RenderCoordinator.done(); |
| |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 1); |
| checkHeaderSectionRow(rows[0], 'server', 'overridden server', true, false, false); |
| addHeaderButton = component.shadowRoot.querySelector('.add-header-button'); |
| assert.isNull(addHeaderButton); |
| }); |
| |
| it('handles rendering and editing \'set-cookie\' headers', async () => { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`https://www.example.com/index.html`, urlString``, null, |
| null, null); |
| request.responseHeaders = [ |
| {name: 'Cache-Control', value: 'max-age=600'}, |
| {name: 'Z-Header', value: 'zzz'}, |
| ]; |
| request.originalResponseHeaders = [ |
| {name: 'Set-Cookie', value: 'bar=original'}, |
| {name: 'Set-Cookie', value: 'foo=original'}, |
| {name: 'Set-Cookie', value: 'malformed'}, |
| {name: 'Cache-Control', value: 'max-age=600'}, |
| {name: 'Z-header', value: 'zzz'}, |
| ]; |
| request.setCookieHeaders = [ |
| {name: 'Set-Cookie', value: 'bar=original'}, |
| {name: 'Set-Cookie', value: 'foo=overridden'}, |
| {name: 'Set-Cookie', value: 'user=12345'}, |
| {name: 'Set-Cookie', value: 'malformed'}, |
| {name: 'Set-Cookie', value: 'wrong format'}, |
| ]; |
| |
| const headerOverridesFileContent = `[ |
| { |
| "applyTo": "index.html", |
| "headers": [ |
| { |
| "name": "set-cookie", |
| "value": "foo=overridden" |
| }, |
| { |
| "name": "set-cookie", |
| "value": "user=12345" |
| }, |
| { |
| "name": "set-cookie", |
| "value": "wrong format" |
| } |
| ] |
| } |
| ]`; |
| |
| const {component, spy} = await setupHeaderEditingWithRequest(headerOverridesFileContent, request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| assert.lengthOf(rows, 7); |
| assert.isNotNull(rows[0].shadowRoot); |
| checkHeaderSectionRow(rows[0], 'cache-control', 'max-age=600', false, false, true); |
| assert.isNotNull(rows[1].shadowRoot); |
| checkHeaderSectionRow(rows[1], 'set-cookie', 'bar=original', false, false, true); |
| assert.isNotNull(rows[2].shadowRoot); |
| checkHeaderSectionRow(rows[2], 'set-cookie', 'foo=overridden', true, false, true); |
| assert.isNotNull(rows[3].shadowRoot); |
| checkHeaderSectionRow(rows[3], 'set-cookie', 'user=12345', true, false, true); |
| assert.isNotNull(rows[4].shadowRoot); |
| checkHeaderSectionRow(rows[4], 'set-cookie', 'malformed', false, false, true); |
| assert.isNotNull(rows[5].shadowRoot); |
| checkHeaderSectionRow(rows[5], 'set-cookie', 'wrong format', true, false, true); |
| assert.isNotNull(rows[6].shadowRoot); |
| checkHeaderSectionRow(rows[6], 'z-header', 'zzz', false, false, true); |
| |
| await editHeaderRow(component, 2, HeaderAttribute.HEADER_VALUE, 'foo=edited'); |
| const expected = [{ |
| applyTo: 'index.html', |
| headers: [ |
| { |
| name: 'set-cookie', |
| value: 'user=12345', |
| }, |
| { |
| name: 'set-cookie', |
| value: 'wrong format', |
| }, |
| { |
| name: 'set-cookie', |
| value: 'foo=edited', |
| }, |
| ], |
| }]; |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| |
| await editHeaderRow(component, 1, HeaderAttribute.HEADER_VALUE, 'bar=edited'); |
| expected[0].headers.push({name: 'set-cookie', value: 'bar=edited'}); |
| sinon.assert.calledWith(spy.getCall(-1), JSON.stringify(expected, null, 2)); |
| }); |
| |
| it('ignores capitalisation of the `set-cookie` header when marking as overridden', async () => { |
| const request = { |
| sortedResponseHeaders: [ |
| {name: 'set-cookie', value: 'user=123'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => false, |
| originalResponseHeaders: [ |
| {name: 'Set-Cookie', value: 'user=123'}, |
| ], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| |
| assert.isNotNull(rows[0].shadowRoot); |
| assert.strictEqual(rows[0].shadowRoot.querySelector('.header-name')?.textContent?.trim(), 'set-cookie'); |
| assert.strictEqual(rows[0].shadowRoot.querySelector('.header-value')?.textContent?.trim(), 'user=123'); |
| assert.isFalse(rows[0].shadowRoot.querySelector('.row')?.classList.contains('header-overridden')); |
| }); |
| |
| it('does not mark unset headers (which cause the request to be blocked) as overridden', async () => { |
| const request = { |
| sortedResponseHeaders: [ |
| {name: 'abc', value: 'def'}, |
| ], |
| blockedResponseCookies: () => [], |
| wasBlocked: () => true, |
| blockedReason: () => Protocol.Network.BlockedReason.CoepFrameResourceNeedsCoepHeader, |
| originalResponseHeaders: [ |
| {name: 'abc', value: 'def'}, |
| ], |
| setCookieHeaders: [], |
| url: () => 'https://www.example.com/', |
| getAssociatedData: () => null, |
| setAssociatedData: () => {}, |
| } as unknown as SDK.NetworkRequest.NetworkRequest; |
| |
| const component = await renderResponseHeaderSection(request); |
| assert.isNotNull(component.shadowRoot); |
| const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row'); |
| |
| const checkRow = (shadowRoot: ShadowRoot, headerName: string, headerValue: string, isOverride: boolean) => { |
| assert.deepEqual(getCleanTextContentFromElements(shadowRoot, '.header-name'), [headerName]); |
| assert.strictEqual(shadowRoot.querySelector('.header-value')?.textContent?.trim(), headerValue); |
| assert.strictEqual(shadowRoot.querySelector('.row')?.classList.contains('header-overridden'), isOverride); |
| }; |
| |
| assert.isNotNull(rows[0].shadowRoot); |
| checkRow(rows[0].shadowRoot, 'Abc', 'def', false); |
| assert.isNotNull(rows[1].shadowRoot); |
| checkRow(rows[1].shadowRoot, 'not-set Cross-Origin-Embedder-Policy', '', false); |
| }); |
| }); |