// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chai';
import sinon from 'sinon';
import { createTarget, } from '../../testing/EnvironmentHelpers.js';
import { setupLocaleHooks } from '../../testing/LocaleHelpers.js';
import { MockCDPConnection } from '../../testing/MockCDPConnection.js';
import { setupRuntimeHooks } from '../../testing/RuntimeHelpers.js';
import { setupSettingsHooks } from '../../testing/SettingsHelpers.js';
import { TestUniverse } from '../../testing/TestUniverse.js';
import * as Host from '../host/host.js';
import * as Platform from '../platform/platform.js';
import * as SDK from './sdk.js';
const { urlString } = Platform.DevToolsPath;
const initiator = {
    target: null,
    frameId: '123',
    initiatorUrl: Platform.DevToolsPath.EmptyUrlString,
};
/** Creates a PageResourceLoader, TargetManager and Settings instance without installing them as globals */
function setup({ loadOverride, maxConcurrentLoads } = {}) {
    const universe = new TestUniverse({ pageResourceLoaderOptions: { loadOverride: loadOverride ?? null, maxConcurrentLoads } });
    const { pageResourceLoader: loader, settings, targetManager } = universe;
    return { loader, settings, targetManager };
}
describe('PageResourceLoader', () => {
    const foo1Url = urlString `foo1`;
    const foo2Url = urlString `foo2`;
    const foo3Url = urlString `foo3`;
    const loads = [];
    const load = async (url) => {
        loads.push({ url });
        return {
            success: true,
            content: `${url} - content`,
            errorDescription: { message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true },
        };
    };
    beforeEach(() => {
        loads.length = 0;
    });
    setupLocaleHooks();
    setupSettingsHooks();
    it('registers extension loads', async () => {
        const { loader } = setup({ loadOverride: load });
        const initiator = {
            extensionId: '123',
            initiatorUrl: urlString `www.test.com/main.wasm.dwp`,
            target: null,
            frameId: null,
        };
        const extensionResource = {
            url: urlString `main.wasm.dwp`,
            success: true,
            initiator,
            size: null,
            duration: null,
        };
        loader.resourceLoadedThroughExtension(extensionResource);
        assert.deepEqual(loader.getScopedNumberOfResources(), { loading: 0, resources: 1 });
        assert.deepEqual(loader.getNumberOfResources(), { loading: 0, queued: 0, resources: 1 });
        const resources = Array.from(loader.getResourcesLoaded().values());
        assert.lengthOf(resources, 1);
        assert.isTrue(resources[0].success);
        assert.deepEqual(resources[0].initiator, initiator);
    });
    it('loads resources correctly', async () => {
        const { loader } = setup({ loadOverride: load });
        const loading = [
            loader.loadResource(foo1Url, initiator),
            loader.loadResource(foo2Url, initiator),
            loader.loadResource(foo3Url, initiator),
        ];
        assert.deepEqual(loader.getNumberOfResources(), { loading: 3, queued: 0, resources: 3 });
        const results = await Promise.all(loading);
        assert.deepEqual(loads.map(x => x.url), ['foo1', 'foo2', 'foo3']);
        assert.deepEqual(results.map(x => x.content), ['foo1 - content', 'foo2 - content', 'foo3 - content']);
        assert.deepEqual(loader.getNumberOfResources(), { loading: 0, queued: 0, resources: 3 });
        const resources = Array.from(loader.getResourcesLoaded().values());
        assert.isTrue(resources.every(x => x.success));
    });
    it('deals with page reloads correctly', async () => {
        const { loader } = setup({ loadOverride: load, maxConcurrentLoads: 1 });
        const loading = [
            loader.loadResource(foo1Url, initiator).catch(e => e.message),
            loader.loadResource(foo2Url, initiator).catch(e => e.message),
            loader.loadResource(foo3Url, initiator).catch(e => e.message),
        ];
        assert.deepEqual(loader.getNumberOfResources(), { loading: 3, queued: 2, resources: 3 });
        loader.onPrimaryPageChanged({
            data: {
                frame: {
                    isOutermostFrame() {
                        return true;
                    },
                    resourceTreeModel() {
                        return {
                            target() {
                                return null;
                            },
                        };
                    },
                },
                type: "Navigation" /* SDK.ResourceTreeModel.PrimaryPageChangeType.NAVIGATION */,
            },
        });
        assert.deepEqual(loader.getNumberOfResources(), { loading: 3, queued: 0, resources: 0 });
        const results = await Promise.all(loading);
        assert.deepEqual(loads.map(x => x.url), ['foo1']);
        assert.deepEqual(results[0].content, 'foo1 - content');
        assert.deepEqual(results[1], 'Load canceled due to reload of inspected page');
        assert.deepEqual(results[2], 'Load canceled due to reload of inspected page');
    });
    it('respects the max concurrent loads', async () => {
        const { loader } = setup({ loadOverride: load, maxConcurrentLoads: 2 });
        const loading = [
            loader.loadResource(foo1Url, initiator),
            loader.loadResource(foo2Url, initiator),
            loader.loadResource(foo3Url, initiator),
        ];
        assert.deepEqual(loader.getNumberOfResources(), { loading: 3, queued: 1, resources: 3 });
        const results = await Promise.all(loading);
        assert.deepEqual(loads.map(x => x.url), ['foo1', 'foo2', 'foo3']);
        assert.deepEqual(results.map(x => x.content), ['foo1 - content', 'foo2 - content', 'foo3 - content']);
        assert.deepEqual(loader.getNumberOfResources(), { loading: 0, queued: 0, resources: 3 });
        const resources = Array.from(loader.getResourcesLoaded().values());
        assert.isTrue(resources.every(x => x.success));
    });
});
// Loading via host bindings requires the settings infra to be booted.
describe('PageResourceLoader', () => {
    setupSettingsHooks();
    it('blocks UNC file paths with the default setting', async () => {
        if (!Host.Platform.isWin()) {
            return;
        }
        const { loader } = setup({ maxConcurrentLoads: 1 });
        const message = await loader.loadResource(urlString `file:////127.0.0.1/share/source-map.js.map`, initiator)
            .catch(e => e.message);
        assert.include(message, 'remote file');
    });
    it('blocks remote file paths with the default setting', async () => {
        const { loader } = setup({ maxConcurrentLoads: 1 });
        const message = await loader.loadResource(urlString `file://host/source-map.js.map`, initiator).catch(e => e.message);
        assert.include(message, 'remote file');
    });
    it('blocks UNC file paths with a backslash on Windows with the default setting', async () => {
        if (!Host.Platform.isWin()) {
            return;
        }
        const { loader } = setup({ maxConcurrentLoads: 1 });
        const message = await loader.loadResource(urlString `file:///\\127.0.0.1/share/source-map.js.map`, initiator)
            .catch(e => e.message);
        assert.include(message, 'remote file');
    });
    it('blocks UNC file paths with path traversal on Windows with the default setting', async () => {
        if (!Host.Platform.isWin()) {
            return;
        }
        const { loader } = setup({ maxConcurrentLoads: 1 });
        const message = await loader.loadResource(urlString `file:///abc\\..//smb-server/share/source-map.js.map'`, initiator)
            .catch(e => e.message);
        assert.include(message, 'remote file');
    });
    it('allows remote file paths with the setting enabled', async () => {
        const { loader, settings } = setup({ maxConcurrentLoads: 1 });
        sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource')
            .callsFake((_url, _headers, streamId, callback) => {
            Host.ResourceLoader.streamWrite(streamId, 'content of the source map');
            callback({ statusCode: 200 });
        });
        settings.moduleSetting('network.enable-remote-file-loading').set(true);
        const response = await loader.loadResource(urlString `file://host/source-map.js.map`, initiator);
        assert.strictEqual(response.content, 'content of the source map');
    });
    it('allows UNC paths on Windows with the setting enabled', async () => {
        if (!Host.Platform.isWin()) {
            return;
        }
        const { loader, settings } = setup({ maxConcurrentLoads: 1 });
        sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource')
            .callsFake((_url, _headers, streamId, callback) => {
            Host.ResourceLoader.streamWrite(streamId, 'content of the source map');
            callback({ statusCode: 200 });
        });
        settings.moduleSetting('network.enable-remote-file-loading').set(true);
        const response = await loader.loadResource(urlString `file:////127.0.0.1/share/source-map.js.map`, initiator);
        assert.strictEqual(response.content, 'content of the source map');
    });
});
describe('PageResourceLoader', () => {
    setupRuntimeHooks();
    setupSettingsHooks();
    describe('loadResource', () => {
        const stream = 'STREAM_ID';
        const initiatorUrl = urlString `htp://example.com`;
        const url = urlString `${`${initiatorUrl}/test.txt`}`;
        function setupLoadingSourceMapsAsNetworkResource(connection) {
            return new Promise(resolve => {
                let contentToRead = 'foo';
                connection.setSuccessHandler('IO.read', () => {
                    const data = contentToRead;
                    contentToRead = null;
                    return { data };
                });
                connection.setSuccessHandler('IO.close', () => ({}));
                connection.setSuccessHandler('Network.loadNetworkResource', request => {
                    resolve(request);
                    return { resource: { success: true, stream, statusCode: 200 } };
                });
            });
        }
        for (const disableCache of [true, false]) {
            it(`loads with ${disableCache ? 'disabled' : 'enabled'} cache based on the setting`, async () => {
                const { loader, settings, targetManager } = setup();
                settings.moduleSetting('cache-disabled').set(disableCache);
                const connection = new MockCDPConnection();
                const target = createTarget({ connection, targetManager });
                const initiator = { target, frameId: null, initiatorUrl };
                const [{ options }, { content }] = await Promise.all([
                    setupLoadingSourceMapsAsNetworkResource(connection),
                    loader.loadResource(url, initiator),
                ]);
                // Check that we loaded the resources with appropriately enabled caching.
                assert.strictEqual(options.disableCache, disableCache);
                // Sanity check on the content.
                assert.deepEqual(content, 'foo');
            });
        }
    });
    describe('loadResource with CSP', () => {
        it('does not fall back to host bindings if frame has restrictive CSP', async () => {
            const { loader, settings, targetManager } = setup();
            settings.moduleSetting('cache-disabled').set(false);
            const connection = new MockCDPConnection();
            connection.setSuccessHandler('Network.getSecurityIsolationStatus', () => {
                return {
                    status: {
                        csp: [{
                                effectiveDirectives: 'connect-src \'none\'',
                                isEnforced: true,
                                source: 'HTTP',
                            }],
                    },
                };
            });
            connection.setFailureHandler('Network.loadNetworkResource', () => {
                return {
                    code: -32000,
                    message: 'Frame not found',
                };
            });
            const target = createTarget({ connection, targetManager });
            const initiator = { target, frameId: '123', initiatorUrl: urlString `https://example.com` };
            const url = urlString `https://example.com/source.map`;
            const loadHostBindingsStub = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource');
            try {
                await loader.loadResource(url, initiator);
                assert.fail('Expected loadResource to throw');
            }
            catch (e) {
                assert.strictEqual(e.message, 'Frame not found');
            }
            // Verify fallback was NOT called
            sinon.assert.notCalled(loadHostBindingsStub);
        });
        it('falls back to host bindings if frame has no restrictive CSP', async () => {
            const { loader, settings, targetManager } = setup();
            settings.moduleSetting('cache-disabled').set(false);
            const connection = new MockCDPConnection();
            connection.setSuccessHandler('Network.getSecurityIsolationStatus', () => {
                return {
                    status: {
                        csp: [],
                    },
                };
            });
            connection.setFailureHandler('Network.loadNetworkResource', () => {
                return {
                    code: -32000,
                    message: 'Frame not found',
                };
            });
            const target = createTarget({ connection, targetManager });
            const initiator = { target, frameId: '123', initiatorUrl: urlString `https://example.com` };
            const url = urlString `https://example.com/source.map`;
            const loadHostBindingsStub = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'loadNetworkResource')
                .callsFake((_url, _headers, streamId, callback) => {
                Host.ResourceLoader.streamWrite(streamId, 'fallback content');
                callback({ statusCode: 200 });
            });
            const result = await loader.loadResource(url, initiator);
            assert.strictEqual(result.content, 'fallback content');
            sinon.assert.calledOnce(loadHostBindingsStub);
        });
    });
});
describe('PageResourceLoader', () => {
    const initiatorUrl = urlString `htp://example.com`;
    const foo1Url = urlString `foo1`;
    const foo2Url = urlString `foo2`;
    const foo3Url = urlString `foo3`;
    setupLocaleHooks();
    setupSettingsHooks();
    setupRuntimeHooks();
    it('handles scoped resources', async () => {
        const load = async () => {
            await new Promise(() => { });
            return {
                success: true,
                content: 'content',
                errorDescription: { message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true },
            };
        };
        const { targetManager, loader } = setup({ loadOverride: load });
        const connection = new MockCDPConnection();
        const target = createTarget({ id: 'main', connection, targetManager });
        const prerenderTarget = createTarget({ id: 'prerender', connection, targetManager });
        const initiator = { target, frameId: null, initiatorUrl };
        const prerenderInitiator = { target: prerenderTarget, frameId: null, initiatorUrl };
        void loader.loadResource(foo1Url, initiator);
        void loader.loadResource(foo2Url, initiator);
        void loader.loadResource(foo3Url, prerenderInitiator);
        assert.deepEqual(loader.getNumberOfResources(), { loading: 3, queued: 0, resources: 3 });
        assert.deepEqual(loader.getScopedNumberOfResources(), { loading: 2, resources: 2 });
        let resources = loader.getScopedResourcesLoaded();
        let resourceUrls = [...resources.values()].map(x => x.url);
        assert.deepEqual(resourceUrls, [foo1Url, foo2Url]);
        targetManager.setScopeTarget(prerenderTarget);
        assert.deepEqual(loader.getScopedNumberOfResources(), { loading: 1, resources: 1 });
        resources = loader.getScopedResourcesLoaded();
        resourceUrls = [...resources.values()].map(x => x.url);
        assert.deepEqual(resourceUrls, [foo3Url]);
    });
    it('handles prerender activation', async () => {
        const load = async (url) => {
            return {
                success: true,
                content: `${url} - content`,
                errorDescription: { message: '', statusCode: 0, netError: 0, netErrorName: '', urlValid: true },
            };
        };
        const { targetManager, loader } = setup({ loadOverride: load });
        const connection = new MockCDPConnection();
        const target = createTarget({ id: 'main', connection, targetManager });
        const prerenderTarget = createTarget({ id: 'prerender', connection, targetManager });
        const initiator = { target, frameId: null, initiatorUrl };
        const prerenderInitiator = { target: prerenderTarget, frameId: null, initiatorUrl };
        await Promise.all([
            loader.loadResource(foo1Url, initiator),
            loader.loadResource(foo2Url, initiator),
            loader.loadResource(foo3Url, prerenderInitiator),
        ]);
        assert.deepEqual(loader.getNumberOfResources(), { loading: 0, queued: 0, resources: 3 });
        assert.deepEqual(loader.getScopedNumberOfResources(), { loading: 0, resources: 2 });
        let resources = loader.getScopedResourcesLoaded();
        let resourceUrls = [...resources.values()].map(x => x.url);
        assert.deepEqual(resourceUrls, [foo1Url, foo2Url]);
        loader.onPrimaryPageChanged({
            data: {
                frame: {
                    isOutermostFrame() {
                        return true;
                    },
                    resourceTreeModel() {
                        return {
                            target() {
                                return prerenderTarget;
                            },
                        };
                    },
                },
                type: "Activation" /* SDK.ResourceTreeModel.PrimaryPageChangeType.ACTIVATION */,
            },
        });
        assert.deepEqual(loader.getNumberOfResources(), { loading: 0, queued: 0, resources: 1 });
        targetManager.setScopeTarget(prerenderTarget);
        assert.deepEqual(loader.getScopedNumberOfResources(), { loading: 0, resources: 1 });
        resources = loader.getScopedResourcesLoaded();
        resourceUrls = [...resources.values()].map(x => x.url);
        assert.deepEqual(resourceUrls, [foo3Url]);
    });
});
//# sourceMappingURL=PageResourceLoader.test.js.map