chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {afterEach, beforeEach, describe, it} from 'node:test';
|
||||
|
||||
import {NetworkFormatter} from '../../src/formatters/NetworkFormatter.js';
|
||||
import type {HTTPRequest} from '../../src/third_party/index.js';
|
||||
import {getMockRequest, getMockResponse} from '../utils.js';
|
||||
|
||||
describe('NetworkFormatter', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'network-formatter-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('works', async () => {
|
||||
const request = getMockRequest();
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [pending]',
|
||||
);
|
||||
});
|
||||
it('shows correct method', async () => {
|
||||
const request = getMockRequest({method: 'POST'});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 POST http://example.com [pending]',
|
||||
);
|
||||
});
|
||||
it('shows correct status for request with response code in 200', async () => {
|
||||
const response = getMockResponse();
|
||||
const request = getMockRequest({response});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [200]',
|
||||
);
|
||||
});
|
||||
it('shows correct status for request with response code in 100', async () => {
|
||||
const response = getMockResponse({
|
||||
status: 199,
|
||||
});
|
||||
const request = getMockRequest({response});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [199]',
|
||||
);
|
||||
});
|
||||
it('shows correct status for request with response code above 200', async () => {
|
||||
const response = getMockResponse({
|
||||
status: 300,
|
||||
});
|
||||
const request = getMockRequest({response});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [300]',
|
||||
);
|
||||
});
|
||||
it('shows correct status for request that failed', async () => {
|
||||
const request = getMockRequest({
|
||||
failure() {
|
||||
return {
|
||||
errorText: 'Error in Network',
|
||||
};
|
||||
},
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [Error in Network]',
|
||||
);
|
||||
});
|
||||
|
||||
it('marks requests selected in DevTools UI', async () => {
|
||||
const request = getMockRequest();
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
selectedInDevToolsUI: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
formatter.toString(),
|
||||
'reqid=1 GET http://example.com [pending] [selected in the DevTools Network panel]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toStringDetailed', () => {
|
||||
it('works with request body from fetchPostData', async () => {
|
||||
const request = getMockRequest({
|
||||
hasPostData: true,
|
||||
postData: undefined,
|
||||
fetchPostData: Promise.resolve('test'),
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 200,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toStringDetailed();
|
||||
assert.match(result, /test/);
|
||||
});
|
||||
|
||||
it('works with request body from postData', async () => {
|
||||
const request = getMockRequest({
|
||||
postData: JSON.stringify({
|
||||
request: 'body',
|
||||
}),
|
||||
hasPostData: true,
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 200,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toStringDetailed();
|
||||
|
||||
assert.match(
|
||||
result,
|
||||
new RegExp(
|
||||
JSON.stringify({
|
||||
request: 'body',
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates request body', async () => {
|
||||
const request = getMockRequest({
|
||||
postData: 'some text that is longer than expected',
|
||||
hasPostData: true,
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 20,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toStringDetailed();
|
||||
assert.match(result, /some text/);
|
||||
});
|
||||
|
||||
it('should save bodies to file when file paths are provided', async () => {
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
url: () => 'http://example.com',
|
||||
headers: () => ({}),
|
||||
hasPostData: () => true,
|
||||
postData: () => 'request body',
|
||||
response: () => ({
|
||||
status: () => 200,
|
||||
headers: () => ({}),
|
||||
buffer: async () => Buffer.from('response body'),
|
||||
}),
|
||||
failure: () => null,
|
||||
redirectChain: () => [],
|
||||
fetchPostData: async () => undefined,
|
||||
} as unknown as HTTPRequest;
|
||||
|
||||
const reqPath = join(tmpDir, 'test_req_' + Date.now());
|
||||
const resPath = join(tmpDir, 'test_res_' + Date.now());
|
||||
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
fetchData: true,
|
||||
requestFilePath: reqPath,
|
||||
responseFilePath: resPath,
|
||||
saveFile: async (data, filename) => {
|
||||
await writeFile(filename, data);
|
||||
return {filename};
|
||||
},
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const json = formatter.toJSONDetailed() as {
|
||||
requestBody: string;
|
||||
responseBody: string;
|
||||
requestBodyFilePath: string;
|
||||
responseBodyFilePath: string;
|
||||
};
|
||||
assert.strictEqual(json.requestBodyFilePath, reqPath);
|
||||
assert.strictEqual(json.responseBodyFilePath, resPath);
|
||||
assert.strictEqual(json.requestBody, undefined);
|
||||
assert.strictEqual(json.responseBody, undefined);
|
||||
});
|
||||
|
||||
it('should not truncate large bodies when saving to file', async () => {
|
||||
const largeBody = 'a'.repeat(10005);
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
url: () => 'http://example.com',
|
||||
headers: () => ({}),
|
||||
hasPostData: () => true,
|
||||
postData: () => largeBody,
|
||||
response: () => ({
|
||||
status: () => 200,
|
||||
headers: () => ({}),
|
||||
buffer: async () => Buffer.from(largeBody),
|
||||
}),
|
||||
failure: () => null,
|
||||
redirectChain: () => [],
|
||||
fetchPostData: async () => undefined,
|
||||
} as unknown as HTTPRequest;
|
||||
|
||||
const reqPath = join(tmpDir, 'test_req_large_' + Date.now());
|
||||
const resPath = join(tmpDir, 'test_res_large_' + Date.now());
|
||||
|
||||
await NetworkFormatter.from(request, {
|
||||
fetchData: true,
|
||||
requestFilePath: reqPath,
|
||||
responseFilePath: resPath,
|
||||
saveFile: async (data, filename) => {
|
||||
await writeFile(filename, data);
|
||||
return {filename};
|
||||
},
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const reqContent = await readFile(reqPath, 'utf8');
|
||||
const resContent = await readFile(resPath, 'utf8');
|
||||
|
||||
assert.strictEqual(reqContent, largeBody);
|
||||
assert.strictEqual(resContent, largeBody);
|
||||
});
|
||||
|
||||
it('handles response body', async () => {
|
||||
const response = getMockResponse();
|
||||
response.buffer = () => {
|
||||
return Promise.resolve(Buffer.from(JSON.stringify({response: 'body'})));
|
||||
};
|
||||
const request = getMockRequest({response});
|
||||
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 200,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toStringDetailed();
|
||||
|
||||
assert.match(result, /"response":"body"/);
|
||||
});
|
||||
|
||||
it('handles redirect chain', async t => {
|
||||
const redirectRequest = getMockRequest({
|
||||
url: 'http://example.com/redirect',
|
||||
});
|
||||
const request = getMockRequest({
|
||||
redirectChain: [redirectRequest],
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
requestIdResolver: () => 2,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toStringDetailed();
|
||||
t.assert.snapshot(result);
|
||||
});
|
||||
it('renders the redirect chain in the same order in text and JSON', async () => {
|
||||
// A >=2 element chain makes ordering observable (a single redirect hides
|
||||
// the bug). toStringDetailed() and toJSONDetailed() are emitted from the
|
||||
// same get_network_request call, so they must agree on the order.
|
||||
const first = getMockRequest({url: 'http://example.com/first'});
|
||||
const second = getMockRequest({url: 'http://example.com/second'});
|
||||
const request = getMockRequest({
|
||||
url: 'http://example.com/final',
|
||||
redirectChain: [first, second],
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
requestIdResolver: () => 2,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const text = formatter.toStringDetailed();
|
||||
const json = formatter.toJSONDetailed();
|
||||
|
||||
const textOrder = [
|
||||
...text.matchAll(/http:\/\/example\.com\/(first|second)/g),
|
||||
].map(m => m[0]);
|
||||
const jsonOrder = (json.redirectChain ?? []).map(entry => entry.url);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
textOrder,
|
||||
jsonOrder,
|
||||
`redirect chain order differs between text (${JSON.stringify(textOrder)}) ` +
|
||||
`and JSON (${JSON.stringify(jsonOrder)})`,
|
||||
);
|
||||
});
|
||||
it('shows saved to file message in toStringDetailed', async () => {
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
url: () => 'http://example.com',
|
||||
headers: () => ({}),
|
||||
hasPostData: () => true,
|
||||
postData: () => 'request body',
|
||||
response: () => ({
|
||||
status: () => 200,
|
||||
headers: () => ({}),
|
||||
buffer: async () => Buffer.from('response body'),
|
||||
}),
|
||||
failure: () => null,
|
||||
redirectChain: () => [],
|
||||
fetchPostData: async () => undefined,
|
||||
} as unknown as HTTPRequest;
|
||||
|
||||
const reqPath = join(tmpDir, 'req.txt');
|
||||
const resPath = join(tmpDir, 'res.txt');
|
||||
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
fetchData: true,
|
||||
requestFilePath: reqPath,
|
||||
responseFilePath: resPath,
|
||||
saveFile: async (data, filename) => {
|
||||
await writeFile(filename, data);
|
||||
return {filename};
|
||||
},
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const result = formatter.toStringDetailed();
|
||||
assert.ok(result.includes(`Saved to ${reqPath}.`));
|
||||
assert.ok(result.includes(`Saved to ${resPath}.`));
|
||||
});
|
||||
|
||||
it('handles missing bodies with filepath', async () => {
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
url: () => 'http://example.com',
|
||||
headers: () => ({}),
|
||||
hasPostData: () => true, // Claim we have data
|
||||
postData: () => null, // But returns null
|
||||
response: () => ({
|
||||
status: () => 200,
|
||||
headers: () => ({}),
|
||||
buffer: async () => {
|
||||
throw new Error('Body not available');
|
||||
},
|
||||
}),
|
||||
failure: () => null,
|
||||
redirectChain: () => [],
|
||||
fetchPostData: async () => {
|
||||
throw new Error('Body not available');
|
||||
},
|
||||
} as unknown as HTTPRequest;
|
||||
|
||||
const reqPath = join(tmpDir, 'req_missing.txt');
|
||||
const resPath = join(tmpDir, 'res_missing.txt');
|
||||
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
fetchData: true,
|
||||
requestFilePath: reqPath,
|
||||
responseFilePath: resPath,
|
||||
saveFile: async (data, filename) => {
|
||||
await writeFile(filename, data);
|
||||
return {filename};
|
||||
},
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const result = formatter.toStringDetailed();
|
||||
assert.ok(
|
||||
result.includes(
|
||||
`### Response Body\n<Response body not available anymore>`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON', () => {
|
||||
it('returns structured data', async () => {
|
||||
const request = getMockRequest();
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
selectedInDevToolsUI: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toJSON();
|
||||
assert.deepEqual(result, {
|
||||
requestId: 1,
|
||||
method: 'GET',
|
||||
url: 'http://example.com',
|
||||
status: 'pending',
|
||||
selectedInDevToolsUI: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSONDetailed', () => {
|
||||
it('returns structured detailed data', async () => {
|
||||
const response = getMockResponse();
|
||||
response.buffer = () => Promise.resolve(Buffer.from('response'));
|
||||
const request = getMockRequest({
|
||||
response,
|
||||
postData: 'request',
|
||||
hasPostData: true,
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
const result = formatter.toJSONDetailed();
|
||||
assert.deepEqual(result, {
|
||||
requestId: 1,
|
||||
method: 'GET',
|
||||
url: 'http://example.com',
|
||||
status: '200',
|
||||
selectedInDevToolsUI: undefined,
|
||||
requestHeaders: {
|
||||
'content-size': '10',
|
||||
},
|
||||
requestBody: 'request',
|
||||
requestBodyFilePath: undefined,
|
||||
responseHeaders: {},
|
||||
responseBody: 'response',
|
||||
responseBodyFilePath: undefined,
|
||||
failure: undefined,
|
||||
redirectChain: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('redacts headers', async () => {
|
||||
const response = getMockResponse({
|
||||
headers: {
|
||||
'set-cookie': 'secret=123',
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
});
|
||||
response.buffer = () => Promise.resolve(Buffer.from('response'));
|
||||
const request = getMockRequest({
|
||||
response,
|
||||
headers: {
|
||||
cookie: 'secret=123',
|
||||
'user-agent': 'test',
|
||||
},
|
||||
});
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
requestId: 1,
|
||||
fetchData: true,
|
||||
saveFile: async () => ({filename: ''}),
|
||||
redactNetworkHeaders: true,
|
||||
});
|
||||
const result = formatter.toJSONDetailed();
|
||||
assert.deepEqual(result.requestHeaders, {
|
||||
cookie: '<redacted>',
|
||||
'user-agent': 'test',
|
||||
});
|
||||
assert.deepEqual(result.responseHeaders, {
|
||||
'set-cookie': '<redacted>',
|
||||
'content-type': 'text/plain',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns file paths in structured detailed data', async () => {
|
||||
const request = {
|
||||
method: () => 'POST',
|
||||
url: () => 'http://example.com',
|
||||
headers: () => ({}),
|
||||
hasPostData: () => true,
|
||||
postData: () => 'request body',
|
||||
response: () => ({
|
||||
status: () => 200,
|
||||
headers: () => ({}),
|
||||
buffer: async () => Buffer.from('response body'),
|
||||
}),
|
||||
failure: () => null,
|
||||
redirectChain: () => [],
|
||||
fetchPostData: async () => undefined,
|
||||
} as unknown as HTTPRequest;
|
||||
|
||||
const reqPath = join(tmpDir, 'req_json.txt');
|
||||
const resPath = join(tmpDir, 'res_json.txt');
|
||||
|
||||
const formatter = await NetworkFormatter.from(request, {
|
||||
fetchData: true,
|
||||
requestFilePath: reqPath,
|
||||
responseFilePath: resPath,
|
||||
saveFile: async (data, filename) => {
|
||||
await writeFile(filename, data);
|
||||
return {filename};
|
||||
},
|
||||
redactNetworkHeaders: false,
|
||||
});
|
||||
|
||||
const result = formatter.toJSONDetailed() as {
|
||||
requestBodyFilePath: string;
|
||||
responseBodyFilePath: string;
|
||||
requestBody?: string;
|
||||
responseBody?: string;
|
||||
};
|
||||
|
||||
assert.strictEqual(result.requestBodyFilePath, reqPath);
|
||||
assert.strictEqual(result.responseBodyFilePath, resPath);
|
||||
assert.strictEqual(result.requestBody, undefined);
|
||||
assert.strictEqual(result.responseBody, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user