UNPKG

chrome-devtools-frontend

Version:
990 lines (915 loc) • 35.7 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 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 { createTarget, deinitializeGlobalVars, initializeGlobalVars, } from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import {createWorkspaceProject, setUpEnvironment} from '../../testing/OverridesHelpers.js'; import {setMockResourceTree} from '../../testing/ResourceTreeHelpers.js'; import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js'; import * as Persistence from '../persistence/persistence.js'; import * as Workspace from '../workspace/workspace.js'; const {urlString} = Platform.DevToolsPath; const setUpEnvironmentWithUISourceCode = (url: string, resourceType: Common.ResourceType.ResourceType, project?: Workspace.Workspace.Project) => { const {workspace, networkPersistenceManager} = setUpEnvironment(); if (!project) { project = {id: () => url, type: () => Workspace.Workspace.projectTypes.Network} as Workspace.Workspace.Project; } const uiSourceCode = new Workspace.UISourceCode.UISourceCode(project, urlString`${url}`, resourceType); project.uiSourceCodes = () => [uiSourceCode]; workspace.addProject(project); return {workspace, project, uiSourceCode, networkPersistenceManager}; }; describeWithMockConnection('NetworkPersistenceManager', () => { beforeEach(async () => { SDK.NetworkManager.MultitargetNetworkManager.dispose(); const target = createTarget(); sinon.stub(target.fetchAgent(), 'invoke_enable'); }); it('can create an overridden file with Local Overrides enabled', async () => { const url = 'http://www.example.com/list-fetch.json'; const resourceType = Common.ResourceType.resourceTypes.Document; const {uiSourceCode} = setUpEnvironmentWithUISourceCode(url, resourceType); const networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, []); const saveSpy = sinon.spy(networkPersistenceManager, 'saveUISourceCodeForOverrides'); const actual = await networkPersistenceManager.setupAndStartLocalOverrides(uiSourceCode); saveSpy.restore(); assert.isTrue(saveSpy.calledOnce, 'should override content once'); assert.isTrue(actual, 'should complete override successfully'); }); it('can create an overridden file with Local Overrides folder set up but disabled', async () => { Common.Settings.Settings.instance().moduleSetting('persistence-network-overrides-enabled').set(false); const url = 'http://www.example.com/list-xhr.json'; const resourceType = Common.ResourceType.resourceTypes.Document; const {uiSourceCode} = setUpEnvironmentWithUISourceCode(url, resourceType); const networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, []); const saveSpy = sinon.spy(networkPersistenceManager, 'saveUISourceCodeForOverrides'); const actual = await networkPersistenceManager.setupAndStartLocalOverrides(uiSourceCode); saveSpy.restore(); assert.isTrue(saveSpy.calledOnce, 'should override content once'); assert.isTrue(actual, 'should complete override successfully'); }); }); describeWithMockConnection('NetworkPersistenceManager', () => { it('does not create interception patterns for forbidden URLs', async () => { SDK.NetworkManager.MultitargetNetworkManager.dispose(); const target = createTarget(); const networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, [ {name: 'helloWorld.html', path: 'www.example.com/', content: 'Hello World!'}, {name: 'forbidden.html', path: 'chromewebstore.google.com/', content: 'Chrome Web Store'}, {name: 'flags', path: 'chrome:/', content: 'Chrome Flags'}, {name: 'index.html', path: 'chrome.google.com/', content: 'Chrome'}, {name: 'allowed.html', path: 'www.google.com/', content: 'Google Search'}, ]); const stub = sinon.stub(target.fetchAgent(), 'invoke_enable'); await networkPersistenceManager.updateInterceptionPatternsForTests(); const patterns = stub.lastCall.args[0].patterns; const expected = [ { urlPattern: 'http?://www.example.com/helloWorld.html', requestStage: Protocol.Fetch.RequestStage.Response, }, { urlPattern: 'http?://www.google.com/allowed.html', requestStage: Protocol.Fetch.RequestStage.Response, }, ]; assert.deepEqual(patterns, expected); }); it('recognizes forbidden network URLs', () => { assert.isTrue(Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl( urlString`chrome://version`)); assert.isTrue(Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl( urlString`https://chromewebstore.google.com/index.html`)); assert.isTrue(Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl( urlString`https://chrome.google.com/script.js`)); assert.isFalse(Persistence.NetworkPersistenceManager.NetworkPersistenceManager.isForbiddenNetworkUrl( urlString`https://www.example.com/script.js`)); }); }); describeWithMockConnection('NetworkPersistenceManager', () => { let networkPersistenceManager: Persistence.NetworkPersistenceManager.NetworkPersistenceManager; beforeEach(async () => { SDK.NetworkManager.MultitargetNetworkManager.dispose(); setMockResourceTree(false); const target = createTarget(); networkPersistenceManager = await createWorkspaceProject(urlString`file:///path/to/overrides`, [ { name: '.headers', path: 'www.example.com/', content: `[ { "applyTo": "index.html", "headers": [{ "name": "index-only", "value": "only added to index.html" }] }, { "applyTo": "*.css", "headers": [{ "name": "css-only", "value": "only added to css files" }] }, { "applyTo": "path/to/*.js", "headers": [{ "name": "another-header", "value": "only added to specific path" }] }, { "applyTo": "repeated.html", "headers": [ { "name": "repeated", "value": "first override" }, { "name": "repeated", "value": "second override" } ] } ]`, }, { name: '.headers', path: '', content: `[ { "applyTo": "*", "headers": [{ "name": "age", "value": "overridden" }] } ]`, }, {name: 'helloWorld.html', path: 'www.example.com/', content: 'Hello World!'}, ]); sinon.stub(target.fetchAgent(), 'invoke_enable'); await networkPersistenceManager.updateInterceptionPatternsForTests(); }); it('merges request headers with override without overlap', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/', }, responseHeaders: [ {name: 'server', value: 'DevTools mock server'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'index-only', value: 'only added to index.html'}, {name: 'server', value: 'DevTools mock server'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges request headers with override with overlap', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/index.html', }, responseHeaders: [ {name: 'server', value: 'DevTools mock server'}, {name: 'age', value: '1'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'index-only', value: 'only added to index.html'}, {name: 'server', value: 'DevTools mock server'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges request headers with override with file type wildcard', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/styles.css', }, responseHeaders: [ {name: 'server', value: 'DevTools mock server'}, {name: 'age', value: '1'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'css-only', value: 'only added to css files'}, {name: 'server', value: 'DevTools mock server'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges request headers with override with specific path', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/path/to/script.js', }, responseHeaders: [ {name: 'server', value: 'DevTools mock server'}, {name: 'age', value: '1'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'another-header', value: 'only added to specific path'}, {name: 'server', value: 'DevTools mock server'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges request headers only when domain matches', async () => { const interceptedRequest = { request: { url: 'https://www.web.dev/index.html', }, responseHeaders: [ {name: 'server', value: 'DevTools mock server'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'server', value: 'DevTools mock server'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges headers while leaving muliple headers with the same name unchanged', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/index.html', }, responseHeaders: [ {name: 'repeated', value: 'first'}, {name: 'repeated', value: 'second'}, {name: 'repeated', value: 'third'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'index-only', value: 'only added to index.html'}, {name: 'repeated', value: 'first'}, {name: 'repeated', value: 'second'}, {name: 'repeated', value: 'third'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('merges headers and can override muliple headers with the same name', async () => { const interceptedRequest = { request: { url: 'https://www.example.com/repeated.html', }, responseHeaders: [ {name: 'repeated', value: 'first'}, {name: 'repeated', value: 'second'}, {name: 'repeated', value: 'third'}, ], } as SDK.NetworkManager.InterceptedRequest; const expected = [ {name: 'age', value: 'overridden'}, {name: 'repeated', value: 'first override'}, {name: 'repeated', value: 'second override'}, ]; const actual = await networkPersistenceManager.handleHeaderInterception(interceptedRequest); assert.deepEqual(actual.sort((a, b) => (a.name.localeCompare(b.name))), expected); }); it('translates URLs into raw and encoded paths', async () => { let toTest = [ // Simple tests. { url: 'www.example.com/', raw: 'www.example.com/index.html', encoded: 'www.example.com/index.html', }, { url: 'www.example.com/simple', raw: 'www.example.com/simple', encoded: 'www.example.com/simple', }, { url: 'www.example.com/hello/foo/bar', raw: 'www.example.com/hello/foo/bar', encoded: 'www.example.com/hello/foo/bar', }, { url: 'www.example.com/.', raw: 'www.example.com/.', encoded: 'www.example.com/', }, { url: 'localhost:8090/endswith.', raw: 'localhost:8090/endswith.', encoded: 'localhost:8090/endswith.', }, // Query parameters. { url: 'example.com/fo?o/bar', raw: 'example.com/fo?o%2Fbar', encoded: 'example.com/fo%3Fo%252Fbar', }, { url: 'example.com/foo?/bar', raw: 'example.com/foo?%2Fbar', encoded: 'example.com/foo%3F%252Fbar', }, { url: 'example.com/foo/?bar', raw: 'example.com/foo/?bar', encoded: 'example.com/foo/%3Fbar', }, { url: 'example.com/?foo/bar/3', raw: 'example.com/?foo%2Fbar%2F3', encoded: 'example.com/%3Ffoo%252Fbar%252F3', }, { url: 'example.com/foo/bar/?3hello/bar', raw: 'example.com/foo/bar/?3hello%2Fbar', encoded: 'example.com/foo/bar/%3F3hello%252Fbar', }, {url: 'https://www.example.com/?foo=bar', raw: 'www.example.com/?foo=bar', encoded: 'www.example.com/%3Ffoo=bar'}, { url: 'http://www.example.com/?foo=bar/', raw: 'www.example.com/?foo=bar%2F', encoded: 'www.example.com/%3Ffoo=bar%252F', }, { url: 'http://www.example.com/?foo=bar?', raw: 'www.example.com/?foo=bar?', encoded: 'www.example.com/%3Ffoo=bar%3F', }, // Hash parameters. { url: 'example.com/?foo/bar/3#hello/bar', raw: 'example.com/?foo%2Fbar%2F3', encoded: 'example.com/%3Ffoo%252Fbar%252F3', }, { url: 'example.com/#foo/bar/3hello/bar', raw: 'example.com/index.html', encoded: 'example.com/index.html', }, { url: 'example.com/foo/bar/#?3hello/bar', raw: 'example.com/foo/bar/index.html', encoded: 'example.com/foo/bar/index.html', }, { url: 'example.com/foo.js#', raw: 'example.com/foo.js', encoded: 'example.com/foo.js', }, { url: 'http://www.web.dev/path/page.html#anchor', raw: 'www.web.dev/path/page.html', encoded: 'www.web.dev/path/page.html', }, { url: 'http://www.example.com/file&$*?.html', raw: 'www.example.com/file&$%2A?.html', encoded: 'www.example.com/file&$%252A%3F.html', }, { url: 'localhost:8090/', raw: 'localhost:8090/index.html', encoded: 'localhost:8090/index.html', }, {url: 'localhost:8090/lpt1', raw: 'localhost:8090/lpt1', encoded: 'localhost:8090/lpt1'}, { url: 'example.com/foo .js', raw: 'example.com/foo%20.js', encoded: 'example.com/foo%2520.js', }, { url: 'example.com///foo.js', raw: 'example.com/foo.js', encoded: 'example.com/foo.js', }, { url: 'example.com///', raw: 'example.com/index.html', encoded: 'example.com/index.html', }, // Very long file names. { url: 'example.com' + '/THIS/PATH/IS_MORE_THAN/200/Chars'.repeat(8), raw: 'example.com/longurls/Chars-141a715a', encoded: 'example.com/longurls/Chars-141a715a', }, { url: ('example.com' + '/THIS/PATH/IS_LESS_THAN/200/Chars'.repeat(5)) .slice(0, -1), raw: 'example.com/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Char', encoded: 'example.com/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Chars/THIS/PATH/IS_LESS_THAN/200/Char', }, ]; if (Host.Platform.isWin()) { toTest = [ { url: 'https://www.example.com/?foo=bar', raw: 'www.example.com/%3Ffoo=bar', encoded: 'www.example.com/%253Ffoo=bar', }, { url: 'http://www.web.dev/path/page.html#anchor', raw: 'www.web.dev/path/page.html', encoded: 'www.web.dev/path/page.html', }, { url: 'http://www.example.com/?foo=bar/', raw: 'www.example.com/%3Ffoo=bar%2F', encoded: 'www.example.com/%253Ffoo=bar%252F', }, { url: 'http://www.example.com/?foo=bar?', raw: 'www.example.com/%3Ffoo=bar%3F', encoded: 'www.example.com/%253Ffoo=bar%253F', }, { url: 'http://www.example.com/file&$*?.html', raw: 'www.example.com/file&$%2A%3F.html', encoded: 'www.example.com/file&$%252A%253F.html', }, { url: 'localhost:8090/', raw: 'localhost%3A8090/index.html', encoded: 'localhost%253A8090/index.html', }, // Windows cannot end with . (period) and space. { url: 'example.com/foo.js.', raw: 'example.com/foo.js%2E', encoded: 'example.com/foo.js%252E', }, { url: 'localhost:8090/endswith.', raw: 'localhost%3A8090/endswith%2E', encoded: 'localhost%253A8090/endswith%252E', }, { url: 'example.com/foo.js ', raw: 'example.com/foo.js%20', encoded: 'example.com/foo.js%2520', }, // Reserved filenames on Windows. { url: 'example.com/CON', raw: 'example.com/%43%4F%4E', encoded: 'example.com/%2543%254F%254E', }, { url: 'example.com/cOn', raw: 'example.com/%63%4F%6E', encoded: 'example.com/%2563%254F%256E', }, { url: 'example.com/cOn/hello', raw: 'example.com/%63%4F%6E/hello', encoded: 'example.com/%2563%254F%256E/hello', }, { url: 'example.com/PRN', raw: 'example.com/%50%52%4E', encoded: 'example.com/%2550%2552%254E', }, { url: 'example.com/AUX', raw: 'example.com/%41%55%58', encoded: 'example.com/%2541%2555%2558', }, { url: 'example.com/NUL', raw: 'example.com/%4E%55%4C', encoded: 'example.com/%254E%2555%254C', }, { url: 'example.com/COM1', raw: 'example.com/%43%4F%4D%31', encoded: 'example.com/%2543%254F%254D%2531', }, { url: 'example.com/COM2', raw: 'example.com/%43%4F%4D%32', encoded: 'example.com/%2543%254F%254D%2532', }, { url: 'example.com/COM3', raw: 'example.com/%43%4F%4D%33', encoded: 'example.com/%2543%254F%254D%2533', }, { url: 'example.com/COM4', raw: 'example.com/%43%4F%4D%34', encoded: 'example.com/%2543%254F%254D%2534', }, { url: 'example.com/COM5', raw: 'example.com/%43%4F%4D%35', encoded: 'example.com/%2543%254F%254D%2535', }, { url: 'example.com/COM6', raw: 'example.com/%43%4F%4D%36', encoded: 'example.com/%2543%254F%254D%2536', }, { url: 'example.com/COM7', raw: 'example.com/%43%4F%4D%37', encoded: 'example.com/%2543%254F%254D%2537', }, { url: 'example.com/COM8', raw: 'example.com/%43%4F%4D%38', encoded: 'example.com/%2543%254F%254D%2538', }, { url: 'example.com/COM9', raw: 'example.com/%43%4F%4D%39', encoded: 'example.com/%2543%254F%254D%2539', }, { url: 'localhost:8090/lpt1', raw: 'localhost%3A8090/%6C%70%74%31', encoded: 'localhost%253A8090/%256C%2570%2574%2531', }, { url: 'example.com/LPT1', raw: 'example.com/%4C%50%54%31', encoded: 'example.com/%254C%2550%2554%2531', }, { url: 'example.com/LPT2', raw: 'example.com/%4C%50%54%32', encoded: 'example.com/%254C%2550%2554%2532', }, { url: 'example.com/LPT3', raw: 'example.com/%4C%50%54%33', encoded: 'example.com/%254C%2550%2554%2533', }, { url: 'example.com/LPT4', raw: 'example.com/%4C%50%54%34', encoded: 'example.com/%254C%2550%2554%2534', }, { url: 'example.com/LPT5', raw: 'example.com/%4C%50%54%35', encoded: 'example.com/%254C%2550%2554%2535', }, { url: 'example.com/LPT6', raw: 'example.com/%4C%50%54%36', encoded: 'example.com/%254C%2550%2554%2536', }, { url: 'example.com/LPT7', raw: 'example.com/%4C%50%54%37', encoded: 'example.com/%254C%2550%2554%2537', }, { url: 'example.com/LPT8', raw: 'example.com/%4C%50%54%38', encoded: 'example.com/%254C%2550%2554%2538', }, { url: 'example.com/LPT9', raw: 'example.com/%4C%50%54%39', encoded: 'example.com/%254C%2550%2554%2539', }, ]; } toTest.forEach(testStrings => { assert.deepEqual(networkPersistenceManager.rawPathFromUrl(urlString`${testStrings.url}`), testStrings.raw); assert.deepEqual( networkPersistenceManager.encodedPathFromUrl(urlString`${testStrings.url}`), testStrings.encoded); }); }); it('is aware of which \'.headers\' files are currently active', done => { const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const project = { type: () => Workspace.Workspace.projectTypes.Network, } as Workspace.Workspace.Project; const networkUISourceCode = { url: () => 'https://www.example.com/hello/world/index.html', project: () => project, contentType: () => Common.ResourceType.resourceTypes.Document, } as Workspace.UISourceCode.UISourceCode; project.uiSourceCodes = () => [networkUISourceCode]; const eventURLs: string[] = []; networkPersistenceManager.addEventListener( Persistence.NetworkPersistenceManager.Events.REQUEST_FOR_HEADER_OVERRIDES_FILE_CHANGED, event => { eventURLs.push(event.data.url()); }); workspace.dispatchEventToListeners(Workspace.Workspace.Events.UISourceCodeAdded, networkUISourceCode); assert.isTrue(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/www.example.com/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); assert.isTrue(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); assert.isFalse(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/www.foo.com/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); workspace.dispatchEventToListeners(Workspace.Workspace.Events.ProjectRemoved, project); setTimeout(() => { assert.deepEqual( eventURLs, ['file:///path/to/overrides/.headers', 'file:///path/to/overrides/www.example.com/.headers']); assert.isFalse(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/www.example.com/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); assert.isFalse(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); assert.isFalse(networkPersistenceManager.hasMatchingNetworkUISourceCodeForHeaderOverridesFile({ url: () => 'file:///path/to/overrides/www.foo.com/.headers', project: () => networkPersistenceManager.project(), } as Workspace.UISourceCode.UISourceCode)); done(); }, 0); }); }); describeWithMockConnection('NetworkPersistenceManager', () => { beforeEach(() => { SDK.NetworkManager.MultitargetNetworkManager.dispose(); }); it('updates active state when target detach and attach', async () => { const {networkPersistenceManager} = setUpEnvironment(); const {project} = createFileSystemUISourceCode({url: urlString`file:///tmp`, mimeType: 'text/plain'}); await networkPersistenceManager.setProject(project); const targetManager = SDK.TargetManager.TargetManager.instance(); assert.isNull(targetManager.rootTarget()); assert.isFalse(networkPersistenceManager.active()); const target = await createTarget(); assert.isTrue(networkPersistenceManager.active()); targetManager.removeTarget(target); target.dispose('test'); assert.isFalse(networkPersistenceManager.active()); }); }); describe('NetworkPersistenceManager', () => { before(async () => { await initializeGlobalVars(); }); after(async () => { await deinitializeGlobalVars(); }); it('escapes patterns to be used in RegExes', () => { assert.strictEqual(Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/'), 'www\\.example\\.com/'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/index.html'), 'www\\.example\\.com/index\\.html'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/*'), 'www\\.example\\.com/.*'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/*.js'), 'www\\.example\\.com/.*\\.js'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/file([{with-special$_^chars}])'), 'www\\.example\\.com/file\\(\\[\\{with\\-special\\$_\\^chars\\}\\]\\)'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/page.html?foo=bar'), 'www\\.example\\.com/page\\.html\\?foo=bar'); assert.strictEqual( Persistence.NetworkPersistenceManager.escapeRegex('www.example.com/*?foo=bar'), 'www\\.example\\.com/.*\\?foo=bar'); }); it('detects when the tail of a path matches with a default index file', () => { assert.deepEqual( Persistence.NetworkPersistenceManager.extractDirectoryIndex('index.html'), {head: '', tail: 'index.html'}); assert.deepEqual( Persistence.NetworkPersistenceManager.extractDirectoryIndex('index.htm'), {head: '', tail: 'index.htm'}); assert.deepEqual( Persistence.NetworkPersistenceManager.extractDirectoryIndex('index.php'), {head: '', tail: 'index.php'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('index.ht'), {head: 'index.ht'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('*.html'), {head: '', tail: '*.html'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('*.htm'), {head: '', tail: '*.htm'}); assert.deepEqual( Persistence.NetworkPersistenceManager.extractDirectoryIndex('path/*.html'), {head: 'path/', tail: '*.html'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('foo*.html'), {head: 'foo*.html'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('a*'), {head: 'a*'}); assert.deepEqual(Persistence.NetworkPersistenceManager.extractDirectoryIndex('a/*'), {head: 'a/*'}); }); it('merges headers which do not overlap', () => { const {networkPersistenceManager} = setUpEnvironment(); const baseHeaders = [{ name: 'age', value: '0', }]; const overrideHeaders = [{ name: 'accept-ranges', value: 'bytes', }]; const merged = [ {name: 'accept-ranges', value: 'bytes'}, {name: 'age', value: '0'}, ]; assert.deepEqual(networkPersistenceManager.mergeHeaders(baseHeaders, overrideHeaders), merged); }); it('merges headers which overlap', () => { const {networkPersistenceManager} = setUpEnvironment(); const baseHeaders = [{ name: 'age', value: '0', }]; const overrideHeaders = [ {name: 'accept-ranges', value: 'bytes'}, {name: 'age', value: '1'}, ]; const merged = [ {name: 'accept-ranges', value: 'bytes'}, {name: 'age', value: '1'}, ]; assert.deepEqual(networkPersistenceManager.mergeHeaders(baseHeaders, overrideHeaders), merged); }); it('generates header patterns', async () => { const {networkPersistenceManager} = setUpEnvironment(); const headers = `[ { "applyTo": "*", "headers": [{ "name": "age", "value": "0" }] }, { "applyTo": "page.html", "headers": [{ "name": "age", "value": "1" }] }, { "applyTo": "index.html", "headers": [{ "name": "age", "value": "2" }] }, { "applyTo": "nested/path/*.js", "headers": [{ "name": "age", "value": "3" }] }, { "applyTo": "*/path/*.js", "headers": [{ "name": "age", "value": "4" }] } ]`; const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/www.example.com/.headers`, content: headers, mimeType: 'text/plain', fileSystemPath: 'file:///path/to/overrides', }); const expectedPatterns = [ 'http?://www.example.com/*', 'http?://www.example.com/page.html', 'http?://www.example.com/index.html', 'http?://www.example.com/', 'http?://www.example.com/nested/path/*.js', 'http?://www.example.com/*/path/*.js', ]; const {headerPatterns, path, overridesWithRegex} = await networkPersistenceManager.generateHeaderPatterns(uiSourceCode); assert.deepEqual(Array.from(headerPatterns).sort(), expectedPatterns.sort()); const expectedMapping = [ { applyTo: /^www\.example\.com\/.*$/.toString(), headers: [{name: 'age', value: '0'}], }, { applyTo: /^www\.example\.com\/page\.html$/.toString(), headers: [{name: 'age', value: '1'}], }, { applyTo: /^www\.example\.com\/(index\.html)?$/.toString(), headers: [{name: 'age', value: '2'}], }, { applyTo: /^www\.example\.com\/nested\/path\/.*\.js$/.toString(), headers: [{name: 'age', value: '3'}], }, { applyTo: /^www\.example\.com\/.*\/path\/.*\.js$/.toString(), headers: [{name: 'age', value: '4'}], }, ]; assert.strictEqual(path, 'www.example.com/'); const actualMapping = overridesWithRegex.map( override => ({applyTo: override.applyToRegex.toString(), headers: override.headers}), ); assert.deepEqual(actualMapping, expectedMapping); }); it('generates header patterns for global header overrides', async () => { const {networkPersistenceManager} = setUpEnvironment(); const headers = `[ { "applyTo": "*", "headers": [{ "name": "age", "value": "0" }] } ]`; const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/.headers`, content: headers, mimeType: 'text/plain', fileSystemPath: 'file:///path/to/overrides', }); const {headerPatterns} = await networkPersistenceManager.generateHeaderPatterns(uiSourceCode); assert.deepEqual(Array.from(headerPatterns), ['http?://*', 'file:///*']); }); it('generates header patterns for long URLs', async () => { const {networkPersistenceManager} = setUpEnvironment(); const headers = `[ { "applyTo": "index.html-5b9f4873.html", "headers": [{ "name": "foo", "value": "bar" }] } ]`; const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/www.longurls.com/longurls/.headers`, content: headers, mimeType: 'text/plain', fileSystemPath: 'file:///path/to/overrides', }); const {headerPatterns, path, overridesWithRegex} = await networkPersistenceManager.generateHeaderPatterns(uiSourceCode); assert.deepEqual(Array.from(headerPatterns), ['http?://www.longurls.com/*']); assert.strictEqual(path, 'www.longurls.com/longurls/'); const expectedMapping = [ { applyTo: /^www\.longurls\.com\/longurls\/index\.html\-5b9f4873\.html$/.toString(), headers: [{name: 'foo', value: 'bar'}], }, ]; const actualMapping = overridesWithRegex.map( override => ({applyTo: override.applyToRegex.toString(), headers: override.headers}), ); assert.deepEqual(actualMapping, expectedMapping); }); it('updates interception patterns upon edit of .headers file', async () => { const {networkPersistenceManager} = setUpEnvironment(); const headers = `[ { "applyTo": "index.html", "headers": [{ "name": "foo", "value": "bar" }] } ]`; const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/www.example.com/.headers`, content: headers, mimeType: 'text/plain', fileSystemPath: 'file:///path/to/overrides', }); const spy = sinon.spy(networkPersistenceManager, 'updateInterceptionPatterns'); sinon.assert.notCalled(spy); uiSourceCode.setWorkingCopy(`[ { "applyTo": "index.html", "headers": [{ "name": "foo2", "value": "bar2" }] } ]`); uiSourceCode.commitWorkingCopy(); sinon.assert.calledOnce(spy); }); });