blob: 663d0067447b38d219a9584b233f606f23750b8a [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import sinon from 'sinon';
import type * as Common from '../core/common/common.js';
import * as Platform from '../core/platform/platform.js';
import * as SDK from '../core/sdk/sdk.js';
import type * as TextUtils from '../core/text_utils/text_utils.js';
import type * as Protocol from '../generated/protocol.js';
import * as Logs from '../models/logs/logs.js';
const {urlString} = Platform.DevToolsPath;
/**
* Configuration options for creating a mock {@link SDK.NetworkRequest.NetworkRequest}.
*/
export interface CreateNetworkRequestOptions {
/** The URL for the request. Defaults to `'https://example.com'`. */
url?: string|Platform.DevToolsPath.UrlString;
/**
* The document/initiator page URL. Defaults to the request's `url`, making the
* request same-origin by default. Specify a different URL or empty string to test
* cross-origin behavior.
*/
documentURL?: string|Platform.DevToolsPath.UrlString;
/** The request identifier. Defaults to `'requestId'`. */
requestId?: string|Protocol.Network.RequestId;
/**
* If true, creates a request via `createWithoutBackendRequest`, which leaves
* `backendRequestId()` undefined and omits frame/loader associations.
*/
withoutBackend?: boolean;
statusCode?: number;
statusText?: string;
requestMethod?: string;
requestHeaders?: SDK.NetworkRequest.NameValue[];
responseHeaders?: SDK.NetworkRequest.NameValue[];
originalResponseHeaders?: SDK.NetworkRequest.NameValue[];
fromMemoryCache?: boolean;
/**
* Content data or a content data provider function. When provided, configures
* `request.setContentDataProvider()`. Pass a function returning `{error: string}`
* to simulate content retrieval failures.
*/
contentData?: TextUtils.ContentData.ContentData|(() => Promise<TextUtils.ContentData.ContentDataOrError>);
/** Flags the request as imported from a HAR archive via `setIsImportedHar()`. */
isImportedHar?: boolean;
mimeType?: string;
resourceType?: Common.ResourceType.ResourceType;
finished?: boolean;
failed?: boolean;
charset?: string;
timing?: Protocol.Network.ResourceTiming;
serviceWorkerRouterInfo?: Protocol.Network.ServiceWorkerRouterInfo;
fetchedViaServiceWorker?: boolean;
initiator?: Protocol.Network.Initiator|null;
frameId?: string|Protocol.Page.FrameId|null;
loaderId?: string|Protocol.Network.LoaderId|null;
}
/**
* Creates and configures a synthetic {@link SDK.NetworkRequest.NetworkRequest} for unit testing.
* Automatically handles string-to-UrlString conversion and provides default values
* so callers only need to specify properties relevant to their test case.
*/
export function createNetworkRequest(options: CreateNetworkRequestOptions = {}): SDK.NetworkRequest.NetworkRequest {
const reqId = (options.requestId ?? 'requestId') as Protocol.Network.RequestId;
const rawUrl = options.url ?? 'https://example.com';
const reqUrl = typeof rawUrl === 'string' ? urlString`${rawUrl}` : rawUrl;
// Default documentURL to reqUrl so that requests are same-origin by default,
// avoiding unintended cross-origin redactions or security checks in tests.
const rawDocUrl = options.documentURL ?? reqUrl;
const docUrl = typeof rawDocUrl === 'string' ? urlString`${rawDocUrl}` : rawDocUrl;
const frameId =
(options.frameId !== undefined && options.frameId !== null) ? options.frameId as Protocol.Page.FrameId : null;
const loaderId = (options.loaderId !== undefined && options.loaderId !== null) ?
options.loaderId as Protocol.Network.LoaderId :
null;
const request = options.withoutBackend ? SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest(
reqId,
reqUrl,
docUrl,
options.initiator ?? null,
) :
SDK.NetworkRequest.NetworkRequest.create(
reqId,
reqUrl,
docUrl,
frameId,
loaderId,
options.initiator ?? null,
);
if (options.statusCode !== undefined) {
request.statusCode = options.statusCode;
}
if (options.statusText !== undefined) {
request.statusText = options.statusText;
}
if (options.requestMethod !== undefined) {
request.requestMethod = options.requestMethod;
}
if (options.requestHeaders) {
request.setRequestHeaders(options.requestHeaders);
}
if (options.responseHeaders) {
request.responseHeaders = options.responseHeaders;
}
if (options.originalResponseHeaders) {
request.originalResponseHeaders = options.originalResponseHeaders;
}
if (options.fromMemoryCache) {
request.setFromMemoryCache();
}
if (options.mimeType !== undefined) {
request.mimeType = options.mimeType;
}
if (options.resourceType !== undefined) {
request.setResourceType(options.resourceType);
}
if (options.finished !== undefined) {
request.finished = options.finished;
}
if (options.failed !== undefined) {
request.failed = options.failed;
}
if (options.charset !== undefined) {
request.setCharset(options.charset);
}
if (options.timing !== undefined) {
request.timing = options.timing;
}
if (options.serviceWorkerRouterInfo !== undefined) {
request.serviceWorkerRouterInfo = options.serviceWorkerRouterInfo;
}
if (options.fetchedViaServiceWorker !== undefined) {
request.fetchedViaServiceWorker = options.fetchedViaServiceWorker;
}
if (options.contentData) {
const dataOrFn = options.contentData;
request.setContentDataProvider(typeof dataOrFn === 'function' ? dataOrFn : () => Promise.resolve(dataOrFn));
}
if (options.isImportedHar !== undefined) {
request.setIsImportedHar(options.isImportedHar);
}
return request;
}
/**
* Configuration options for {@link stubInitiatorGraph}.
*/
export interface StubInitiatorGraphOptions {
/**
* Upstream requests that triggered the target request. Do not include the target
* request itself; it is automatically added as the primary initiator.
* Defaults to a single synthetic cross-origin initiator.
*/
initiators?: SDK.NetworkRequest.NetworkRequest[];
/**
* Downstream requests triggered by the target request.
* Defaults to two synthetic cross-origin child requests.
*/
initiated?: SDK.NetworkRequest.NetworkRequest[];
/**
* The NetworkLog instance on which to install the Sinon stub.
* Defaults to `Logs.NetworkLog.NetworkLog.instance()`.
*/
networkLog?: Logs.NetworkLog.NetworkLog;
}
/**
* Stubs {@link Logs.NetworkLog.NetworkLog.initiatorGraphForRequest} for the given request
* using Sinon.
*
* If `initiators` or `initiated` lists are omitted, generates default synthetic requests
* simulating a typical multi-level initiator chain with cross-origin boundaries.
*
* @param request The network request whose initiator graph is being queried.
* @param options Custom requests or NetworkLog instance to configure the stub.
* @returns The lists of ancestor (`initiators`) and descendant (`initiated`) requests used,
* along with the Sinon stub.
*/
export function stubInitiatorGraph(
request: SDK.NetworkRequest.NetworkRequest,
options: StubInitiatorGraphOptions = {},
): {
initiators: SDK.NetworkRequest.NetworkRequest[],
initiated: SDK.NetworkRequest.NetworkRequest[],
stub: sinon.SinonStub<[SDK.NetworkRequest.NetworkRequest], Logs.NetworkLog.InitiatorGraph>,
} {
// Explicitly use EmptyUrlString so that default synthetic requests are treated as
// cross-origin, exercising URL redaction and security checks in downstream formatters.
const initiators = options.initiators ?? [
createNetworkRequest({
requestId: 'requestId-initiator',
url: urlString`https://www.initiator.com`,
documentURL: Platform.DevToolsPath.EmptyUrlString,
}),
];
const initiated = options.initiated ?? [
createNetworkRequest({
requestId: 'requestId-initiated-1',
url: urlString`https://www.example.com/1`,
documentURL: Platform.DevToolsPath.EmptyUrlString,
}),
createNetworkRequest({
requestId: 'requestId-initiated-2',
url: urlString`https://www.example.com/2`,
documentURL: Platform.DevToolsPath.EmptyUrlString,
}),
];
const networkLog = options.networkLog ?? Logs.NetworkLog.NetworkLog.instance();
const stub = sinon.stub(networkLog, 'initiatorGraphForRequest');
// Default fallback for any request not explicitly handled:
stub.returns({
initiators: new Set<SDK.NetworkRequest.NetworkRequest>(),
initiated: new Map<SDK.NetworkRequest.NetworkRequest, SDK.NetworkRequest.NetworkRequest>(),
});
// InitiatorGraph.initiated is a Map<ChildRequest, ParentInitiator>:
// - [request, init]: target request was initiated by upstream ancestor `init`.
// - [init, request]: downstream child `init` was initiated by target `request`.
stub.withArgs(request).returns({
initiators: new Set([request, ...initiators]),
initiated: new Map([
...initiators.map(init => [request, init] as const),
...initiated.map(init => [init, request] as const),
]),
});
// Downstream consumers may recursively query initiatorGraphForRequest on child requests:
for (const init of initiated) {
stub.withArgs(init).returns({
initiators: new Set([]),
initiated: new Map([[init, request]]),
});
}
return {initiators, initiated, stub};
}