diff --git a/packages/insomnia-inso/src/cli.test.ts b/packages/insomnia-inso/src/cli.test.ts index 30e7a807fb..f35eae7b11 100644 --- a/packages/insomnia-inso/src/cli.test.ts +++ b/packages/insomnia-inso/src/cli.test.ts @@ -1,5 +1,7 @@ import type { ExecException } from 'node:child_process'; import { exec } from 'node:child_process'; +import fs from 'node:fs'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -144,8 +146,7 @@ describe('inso dev bundle', () => { }); it('send request with client cert and key', async () => { - const input = - `$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/db/fixtures/nedb --requestNamePattern "withCertAndCA" --verbose "Insomnia Designer" wrk_0b96eff -f $PWD/packages`; + const input = `$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/db/fixtures/nedb --requestNamePattern "withCertAndCA" --verbose "Insomnia Designer" wrk_0b96eff -f $PWD/packages`; const result = await runCliFromRoot(input); if (result.code !== 0) { console.log(result); @@ -186,6 +187,69 @@ describe('inso dev bundle', () => { expect(result.stdout).toContain('updated value from folder: 666'); }); }); + + describe('run collection report generation', () => { + it.each([ + { + name: 'default report', + input: + '$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/run-collection-result-report.yml wrk_c5d5b5 -e env_1072af', + expectedReportFile: './fixtures/run-collection-report/default-report.json', + }, + { + name: 'redact report', + input: + '$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/run-collection-result-report.yml wrk_c5d5b5 -e env_1072af --includeFullData=redact --acceptRisk', + expectedReportFile: './fixtures/run-collection-report/redact-report.json', + }, + { + name: 'plaintext report', + input: + '$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/run-collection-result-report.yml wrk_c5d5b5 -e env_1072af --includeFullData=plaintext --acceptRisk', + expectedReportFile: './fixtures/run-collection-report/plaintext-report.json', + }, + ])('generate report: $name', async ({ input, expectedReportFile }) => { + const root = path.join(tmpdir(), 'insomnia-cli-test-output'); + const outputFilePath = path.resolve(root, 'run-collection-report-output.json'); + + const result = await runCliFromRoot(`${input} --output ${outputFilePath}`); + expect(result.code).toBe(0); + + const expectedReport = JSON.parse(fs.readFileSync(path.resolve(__dirname, expectedReportFile), 'utf8')); + expect(fs.existsSync(outputFilePath)).toBe(true); + const report = JSON.parse(fs.readFileSync(outputFilePath, 'utf8')); + + // Some fields are dynamic so we use expect.any to validate their types/ existence + expect(report).toEqual({ + ...expectedReport, + executions: expectedReport.executions.map((exec: any) => ({ + ...exec, + response: { + ...exec.response, + // executionTime can vary so just check it's a number + responseTime: expect.any(Number), + headers: exec.response.headers + ? { + ...exec.response.headers, + date: expect.any(String), + } + : undefined, + }, + tests: exec.tests.map((test: any) => ({ + ...test, + executionTime: expect.any(Number), + })), + })), + timing: { + started: expect.any(Number), + completed: expect.any(Number), + responseAverage: expect.any(Number), + responseMin: expect.any(Number), + responseMax: expect.any(Number), + }, + }); + }); + }); }); const packagedSuccessCodes = shouldReturnSuccessCode.map(x => diff --git a/packages/insomnia-inso/src/cli.ts b/packages/insomnia-inso/src/cli.ts index 40aea7ce15..a2af13b7ca 100644 --- a/packages/insomnia-inso/src/cli.ts +++ b/packages/insomnia-inso/src/cli.ts @@ -1,12 +1,15 @@ import fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import { homedir } from 'node:os'; -import path from 'node:path'; +import path, { dirname } from 'node:path'; import * as commander from 'commander'; import type { logType } from 'consola'; import consola, { BasicReporter, FancyReporter, LogLevel } from 'consola'; import { cosmiconfig } from 'cosmiconfig'; +// @ts-expect-error the enquirer types are incomplete https://github.com/enquirer/enquirer/pull/307 +import { Confirm } from 'enquirer'; +import { pick } from 'es-toolkit'; import { isDevelopment, JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from 'insomnia/src/common/constants'; import { getSendRequestCallbackMemDb } from 'insomnia/src/common/send-request'; import type { Environment, UserUploadEnvironment } from 'insomnia/src/models/environment'; @@ -19,10 +22,13 @@ import orderedJSON from 'json-order'; import { parseArgsStringToArgv } from 'string-argv'; import { v4 as uuidv4 } from 'uuid'; +import type { Workspace } from '~/models/workspace'; + import { type RequestTestResult } from '../../insomnia-scripting-environment/src/objects'; import packageJson from '../package.json'; import { exportSpecification, writeFileWithCliOptions } from './commands/export-specification'; import { getRuleSetFileFromFolderByFilename, lintSpecification } from './commands/lint-specification'; +import { RunCollectionResultReport } from './commands/run-collection/result-report'; import type { Database } from './db'; import { isFile, loadDb } from './db'; import { insomniaExportAdapter } from './db/adapters/insomnia-adapter'; @@ -140,7 +146,18 @@ export const getDefaultProductName = (): string => { const localAppDir = getAppDataDir(getDefaultProductName()); export const getAbsoluteFilePath = ({ workingDir, file }: { workingDir?: string; file: string }) => { - return file && path.resolve(workingDir || process.cwd(), file); + if (!file) { + return ''; + } + + if (workingDir) { + if (fs.existsSync(workingDir) && !fs.statSync(workingDir).isDirectory()) { + return path.resolve(dirname(workingDir), file); + } + return path.resolve(workingDir, file); + } + + return path.resolve(process.cwd(), file); }; export const logErrorAndExit = (err?: Error) => { if (err instanceof InsoError) { @@ -533,6 +550,18 @@ export const go = (args?: string[]) => { 'This allows you to control what folders Insomnia (and scripts within Insomnia) can read/write to.', [], ) + .option('--output ', 'Output the results to a file in JSON format.') + .addOption( + new commander.Option( + '--includeFullData ', + 'Include full data in the output file, including request, response, environment and etc.', + ).choices(['redact', 'plaintext']), + ) + .option( + '--acceptRisk', + 'Accept the security warning when outputting to a file, please make sure you understand the risks.', + false, + ) .action( async ( identifier, @@ -552,12 +581,57 @@ export const go = (args?: string[]) => { noProxy?: string; reporter: TestReporter; dataFolders: string[]; + output?: string; + includeFullData?: 'redact' | 'plaintext'; + acceptRisk: boolean; }, ) => { const options = await mergeOptionsAndInit(cmd); - const pathToSearch = getWorkingDir(options); + let outputFilePath = ''; + // Check if the output file is a writable file if it exists + if (options.output) { + outputFilePath = getAbsoluteFilePath({ workingDir: options.workingDir, file: options.output }); + if (fs.existsSync(outputFilePath)) { + const stats = fs.statSync(outputFilePath); + if (!stats.isFile()) { + logger.fatal(`Output path "${outputFilePath}" is not a file.`); + return process.exit(1); + } + try { + fs.accessSync(outputFilePath, fs.constants.W_OK); + } catch (err) { + logger.fatal(`Output file "${outputFilePath}" is not writable.`); + return process.exit(1); + } + } + // Show security disclaimer when outputting to a file with data + if (options.includeFullData && !options.acceptRisk) { + const disclaimerMessage = [ + 'SECURITY WARNING', + 'Outputting to a file could contain sensitive data like API tokens or secrets. Make sure you understand this, and the contents of your collection, before proceeding.', + 'Are you sure you want to continue?', + ].join('\n'); + + const acceptDisclaimer = await new Confirm({ message: disclaimerMessage, initial: false }).run(); + + if (!acceptDisclaimer) { + logger.fatal('User did not accept the disclaimer, aborting.'); + return process.exit(1); + } + } + } + + const report = new RunCollectionResultReport( + { + outputFilePath, + includeFullData: options.includeFullData, + }, + logger, + ); + + const pathToSearch = getWorkingDir(options); const db = await loadDb({ pathToSearch, filterTypes: [], @@ -569,6 +643,8 @@ export const go = (args?: string[]) => { return process.exit(1); } + report.update({ collection: workspace as Workspace }); + // Find environment const workspaceId = workspace._id; // get global env by id from nedb or gitstore, or first element from file @@ -635,6 +711,8 @@ export const go = (args?: string[]) => { return process.exit(1); } + report.update({ environment: environment as Environment }); + let requestsToRun = getRequestsToRunFromListOrWorkspace(db, workspaceId, options.item); if (options.requestNamePattern) { requestsToRun = requestsToRun.filter(req => req.name.match(new RegExp(options.requestNamePattern))); @@ -735,6 +813,13 @@ export const go = (args?: string[]) => { noProxy: options.noProxy, }; + report.update({ + proxy: proxyOptions, + iterationCount, + iterationData, + startedAt: Date.now(), + }); + const sendRequest = await getSendRequestCallbackMemDb( environment._id, db, @@ -762,6 +847,21 @@ export const go = (args?: string[]) => { continue; } + report.addExecution({ + request: req, + response: { + status: res.statusMessage, + code: res.status, + headers: res.headers, + data: res.data, + responseTime: res.responseTime, + }, + // TODO: Remove the category field from test results since it is not needed in the report and is always incorrect as unknown. + tests: res.testResults.map(t => pick(t, ['testCase', 'status', 'executionTime', 'errorMessage'])), + iteration: i, + success, + }); + const timelineString = await readFile(res.timelinePath, 'utf8'); const appendNewLineIfNeeded = (str: string) => (str.endsWith('\n') ? str : str + '\n'); const timeline = deserializeNDJSON(timelineString) @@ -796,8 +896,12 @@ export const go = (args?: string[]) => { logTestResultSummary(testResultsQueue); + await report.saveReport(); return process.exit(success ? 0 : 1); } catch (error) { + report.update({ error: (error instanceof Error ? error.message : String(error)) || 'Unknown error' }); + await report.saveReport(); + logErrorAndExit(error); } return process.exit(1); diff --git a/packages/insomnia-inso/src/commands/run-collection/result-report.ts b/packages/insomnia-inso/src/commands/run-collection/result-report.ts new file mode 100644 index 0000000000..106048cd0f --- /dev/null +++ b/packages/insomnia-inso/src/commands/run-collection/result-report.ts @@ -0,0 +1,282 @@ +import fs from 'node:fs'; +import { dirname } from 'node:path'; + +import type { Consola } from 'consola'; +import { pick } from 'es-toolkit'; + +import type { Environment, UserUploadEnvironment } from '~/models/environment'; +import type { Request, RequestAuthentication, RequestHeader } from '~/models/request'; +import type { Workspace } from '~/models/workspace'; +import { typedKeys } from '~/utils'; + +import type { RequestTestResult } from '../../../../insomnia-scripting-environment/src/objects'; + +interface RunReportExecution { + request: Request; + response: { + status?: string; + code?: number; + headers?: Record; + data?: string; + responseTime: number; + }; + tests: Omit[]; + iteration: number; + success: boolean; +} + +type ReportData = Pick< + RunCollectionResultReport, + 'collection' | 'environment' | 'proxy' | 'iterationCount' | 'iterationData' | 'executions' | 'startedAt' | 'error' +>; + +const insensitiveBaseModelKeys = ['_id', 'type', 'parentId', 'created', 'modified', 'name'] as const; + +export class RunCollectionResultReport { + // The collection (workspace) that was run + collection: Workspace | null = null; + // The environment used during the run + environment: Environment | null = null; + // The proxy settings used during the run, if set + proxy: { + proxyEnabled: boolean; + httpProxy?: string; + httpsProxy?: string; + noProxy?: string; + } | null = null; + // The number of iterations that were run + iterationCount = 0; + // The iteration data used during the run + iterationData: UserUploadEnvironment[] = []; + // The executions that occurred during the run + executions: RunReportExecution[] = []; + // The start time of the run + startedAt: number = Date.now(); + // The error that occurred during the run, if any + error: string | null = null; + + constructor( + private options: { + outputFilePath: string; + includeFullData?: 'redact' | 'plaintext'; + }, + private logger: Consola, + init?: Partial, + ) { + Object.assign(this, init); + } + + update(partial: Partial) { + Object.assign(this, partial); + } + + addExecution(execution: RunReportExecution) { + this.executions.push(execution); + } + + private getTiming() { + const responseTimes = this.executions.map(e => e.response.responseTime); + + return { + started: this.startedAt, + completed: Date.now(), + responseAverage: responseTimes.length ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0, + responseMin: Math.min(...responseTimes), + responseMax: Math.max(...responseTimes), + }; + } + + private getStats() { + const iterationStatusArray = new Array(this.iterationCount).fill(true); + let failedRequests = 0; + let totalTests = 0; + let failedTests = 0; + + for (const exec of this.executions) { + if (exec.success === false) { + iterationStatusArray[exec.iteration] = false; + failedRequests += 1; + } + totalTests += exec.tests.length; + failedTests += exec.tests.filter(test => test.status === 'failed').length; + } + + return { + iterations: { + // The total number of iterations + total: this.iterationCount, + // The number of failed iterations + failed: iterationStatusArray.filter(status => status === false).length, + }, + requests: { + // The total number of requests + total: this.executions.length, + // The number of failed requests + failed: failedRequests, + }, + tests: { + // The total number of tests + total: totalTests, + // The number of failed tests + failed: failedTests, + }, + }; + } + + private getFullData() { + return { + collection: this.collection, + environment: this.environment, + proxy: this.proxy, + // Don't expose the success field + executions: this.executions.map(exec => pick(exec, ['request', 'response', 'tests', 'iteration'])), + timing: this.getTiming(), + stats: this.getStats(), + error: this.error, + }; + } + private getSafeData() { + return { + collection: this.collection, + environment: this.environment ? pick(this.environment, [...insensitiveBaseModelKeys, 'isPrivate']) : null, + proxy: this.proxy, + executions: this.executions.map(exec => ({ + request: pick(exec.request, [...insensitiveBaseModelKeys, 'description']), + response: pick(exec.response, ['status', 'code', 'responseTime']), + tests: exec.tests, + iteration: exec.iteration, + })), + timing: this.getTiming(), + stats: this.getStats(), + error: this.error, + }; + } + private getRedactedData() { + const REDACTED_VALUE = ''; + + // Known sensitive header names (case-insensitive) + const sensitiveHeaders = new Set([ + 'cookie', + 'set-cookie', + 'authorization', + 'auth', + 'x-auth-token', + 'x-api-key', + 'api-key', + 'x-csrf-token', + 'x-xsrf-token', + 'x-access-token', + 'x-refresh-token', + 'bearer', + 'basic', + 'x-forwarded-for', + 'x-real-ip', + 'x-client-ip', + 'proxy-authorization', + ]); + + const redactObject = >(obj: T, keysToRedact?: Set, ignoreCase?: boolean) => { + const redactedObject: Partial = {}; + for (const [key, value] of Object.entries(obj)) { + let needsRedaction = true; + + if (keysToRedact) { + if (ignoreCase) { + const keysToRedactInLowerCase = new Set(Array.from(keysToRedact).map(k => k.toString().toLowerCase())); + needsRedaction = keysToRedactInLowerCase.has(key.toLowerCase()); + } else { + needsRedaction = keysToRedact.has(key); + } + } + + redactedObject[key as keyof T] = needsRedaction ? REDACTED_VALUE : value; + } + return redactedObject; + }; + + const redactRequestHeaders = (headers?: RequestHeader[]) => { + if (!headers) return headers; + + return headers.map(header => ({ + ...header, + value: sensitiveHeaders.has(header.name?.toLowerCase()) ? REDACTED_VALUE : header.value, + })); + }; + + const redactResponseHeaders = (headers?: Record) => { + if (!headers) return headers; + + return redactObject(headers, sensitiveHeaders, true); + }; + + const redactAuth = (auth?: RequestAuthentication | {}) => { + const isValidAuth = (auth?: RequestAuthentication | {}): auth is RequestAuthentication => { + return !!auth && Object.keys(auth).length > 0 && 'type' in auth; + }; + + if (!isValidAuth(auth)) return auth; + const authWhitelist = new Set(['type', 'disabled', 'grantType']); + return redactObject(auth, new Set(typedKeys(auth).filter(k => !authWhitelist.has(k)))); + }; + + const redactEnvironment = (env?: Environment | null) => { + if (!env) { + return env; + } + return { + ...env, + data: redactObject(env.data), + ...(env.kvPairData + ? { + kvPairData: env.kvPairData.map(pair => ({ + ...pair, + value: REDACTED_VALUE, + })), + } + : {}), + }; + }; + + return { + collection: this.collection, + environment: redactEnvironment(this.environment), + proxy: this.proxy, + executions: this.executions.map(exec => ({ + request: { + ...exec.request, + headers: redactRequestHeaders(exec.request.headers), + authentication: redactAuth(exec.request.authentication), + }, + response: { + ...exec.response, + headers: redactResponseHeaders(exec.response.headers), + }, + tests: exec.tests, + iteration: exec.iteration, + })), + timing: this.getTiming(), + stats: this.getStats(), + error: this.error, + }; + } + + private generateJSONReport() { + if (this.options.includeFullData === 'plaintext') { + return this.getFullData(); + } else if (this.options.includeFullData === 'redact') { + return this.getRedactedData(); + } + return this.getSafeData(); + } + + saveReport = async () => { + if (!this.options.outputFilePath) { + return; + } + + const jsonReport = this.generateJSONReport(); + await fs.promises.mkdir(dirname(this.options.outputFilePath), { recursive: true }); + await fs.promises.writeFile(this.options.outputFilePath, JSON.stringify(jsonReport, null, 2), 'utf8'); + this.logger.log('Result report saved to:', this.options.outputFilePath); + }; +} diff --git a/packages/insomnia-inso/src/examples/run-collection-result-report.yml b/packages/insomnia-inso/src/examples/run-collection-result-report.yml new file mode 100644 index 0000000000..fa073347da --- /dev/null +++ b/packages/insomnia-inso/src/examples/run-collection-result-report.yml @@ -0,0 +1,207 @@ +type: collection.insomnia.rest/5.0 +name: CLI Run Collection Report Generation +meta: + id: wrk_c5d5b5167b9049af8b53c5b6f076349b + created: 1762508027498 + modified: 1762508027498 + description: '' +collection: + - url: http://localhost:4010/echo + name: Request With Sensitive Headers + meta: + id: req_a7eea21417aa4af2a1c906d5e562a325 + created: 1754293004564 + modified: 1762509152390 + isPrivate: false + description: '' + sortKey: -1754293004564 + method: POST + body: + mimeType: application/json + text: |- + { + "foo": "bar" + } + parameters: + - name: queryname + value: queryvalue + disabled: false + id: pair_927facef133b4df59bb4661c9473111a + headers: + - name: Content-Type + value: application/json + id: pair_152fb2b378bc46559796642f3dd5c0d0 + - name: User-Agent + value: insomnia/11.4.0 + id: pair_8e55f5504c924f798bf31f1db38412e3 + - name: X-Header + value: foo + id: pair_714ac3cf4ca645b38755a8b318c28b91 + - id: pair_aa7cdfd5c82d4f4cb7737ad41e357220 + name: Authorization + value: + - id: pair_32ad47361f6c40bcb3f84273b1b4ce50 + name: Auth + value: + - id: pair_5dd2022670e04b6ea959ac75ba5ead74 + name: Cookie + value: + - id: pair_245205652e154b30a0ab241c13e61b99 + name: x-auth-token + value: + - id: pair_3e019947ced7433f90dde5c4f69af8d5 + name: x-api-key + value: + authentication: + type: digest + disabled: false + username: '' + password: '' + scripts: + preRequest: | + const track = +(new Date()); + + insomnia.environment.set("track", track); + + insomnia.test('Check if track exists', () => { + insomnia.expect(insomnia.environment.get("track")).to.eql(track); + }); + afterResponse: | + + insomnia.test('Check if status is 200', () => { + insomnia.expect(insomnia.response.code).to.eql(200); + }); + + insomnia.test('Check if track exists', () => { + insomnia.expect(insomnia.environment.get("track")).to.not.null; + }); + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - url: http://localhost:4010/echo + name: Request With Auth - Basic + meta: + id: req_bb9e621055204a13bc9e301fe376748f + created: 1754297299654 + modified: 1762509360358 + isPrivate: false + description: '' + sortKey: -1754297299654 + method: POST + body: + mimeType: multipart/form-data + params: + - id: pair_e6f1d8dad6f1413497c3b2fc4a1c7fc5 + name: key + value: value + description: '' + disabled: false + parameters: + - name: from-r1 + value: "{% response 'body', 'req_a7eea21417aa4af2a1c906d5e562a325', + 'b64::JC5kYXRh::46b', 'no-history', 60 %}" + disabled: false + id: pair_c5d5d7ca8e89414ab14cff81370b45d7 + headers: + - name: Content-Type + value: multipart/form-data + - name: User-Agent + value: insomnia/11.4.0 + authentication: + type: basic + useISO88591: false + disabled: false + username: username + password: password + scripts: + preRequest: | + insomnia.test('Check if track exists', () => { + insomnia.expect(insomnia.environment.get("track")).to.not.null; + }); + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true + - url: http://localhost:4010/echo + name: Request With Auth - API Key + meta: + id: req_ef809d54e6aa4d72a54593fdeaa14b93 + created: 1754468596942 + modified: 1762509338246 + isPrivate: false + description: '' + sortKey: -1754468596942 + method: GET + body: + mimeType: multipart/form-data + parameters: + - id: pair_cd383e4bd88a42389b0f8531a1b699ae + name: '' + value: '' + description: '' + disabled: false + headers: + - name: Content-Type + value: multipart/form-data + - name: User-Agent + value: insomnia/11.4.0 + authentication: + type: apikey + disabled: false + key: api-key + value: api-key-value + addTo: header + scripts: + preRequest: | + + insomnia.expect(200).to.eql(200); + afterResponse: |+ + + insomnia.test('Check if status is 200', () => { + insomnia.expect(insomnia.response.code).to.eql(200); + }); + + settings: + renderRequestBody: true + encodeUrl: true + followRedirects: global + cookies: + send: true + store: true + rebuildPath: true +cookieJar: + name: Default Jar + meta: + id: jar_64efeac28c0b43c79fa15bf19a98e7a6 + created: 1754292952909 + modified: 1762509361179 +environments: + name: Base Environment + meta: + id: env_6aec164bc7c64ffc96c62e06517c5ba6 + created: 1754292952907 + modified: 1762509361181 + isPrivate: false + data: + env-name: env-value + subEnvironments: + - name: New Environment + meta: + id: env_1072af0c15764cd8875f645b0f2e5ad4 + created: 1754293170062 + modified: 1762509361180 + isPrivate: true + sortKey: 1754293170062 + data: + pri-env-name: env-value + track: 1762509003311 + track-3: 1762508357019 diff --git a/packages/insomnia-inso/src/fixtures/run-collection-report/default-report.json b/packages/insomnia-inso/src/fixtures/run-collection-report/default-report.json new file mode 100644 index 0000000000..9ecccf2c9d --- /dev/null +++ b/packages/insomnia-inso/src/fixtures/run-collection-report/default-report.json @@ -0,0 +1,135 @@ +{ + "collection": { + "_id": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1762508027498, + "modified": 1762508027498, + "isPrivate": false, + "description": "", + "metaSortKey": 0, + "type": "Workspace", + "name": "CLI Run Collection Report Generation", + "parentId": "", + "scope": "collection" + }, + "environment": { + "_id": "env_1072af0c15764cd8875f645b0f2e5ad4", + "type": "Environment", + "parentId": "env_6aec164bc7c64ffc96c62e06517c5ba6", + "created": 1754293170062, + "modified": 1762509361180, + "name": "New Environment", + "isPrivate": true + }, + "proxy": { + "proxyEnabled": false, + "httpProxy": "", + "httpsProxy": "", + "noProxy": "" + }, + "executions": [ + { + "request": { + "_id": "req_ef809d54e6aa4d72a54593fdeaa14b93", + "type": "Request", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1754468596942, + "modified": 1762509338246, + "name": "Request With Auth - API Key", + "description": "" + }, + "response": { + "status": "OK", + "code": 200, + "responseTime": 4.039 + }, + "tests": [ + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.11487499999998363 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_bb9e621055204a13bc9e301fe376748f", + "type": "Request", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1754297299654, + "modified": 1762509360358, + "name": "Request With Auth - Basic", + "description": "" + }, + "response": { + "status": "OK", + "code": 200, + "responseTime": 0.596 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.03712500000006003 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_a7eea21417aa4af2a1c906d5e562a325", + "type": "Request", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1754293004564, + "modified": 1762509152390, + "name": "Request With Sensitive Headers", + "description": "" + }, + "response": { + "status": "OK", + "code": 200, + "responseTime": 0.404 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.020999999999958163 + }, + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.04466699999989032 + }, + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.0225830000000542 + } + ], + "iteration": 0 + } + ], + "timing": { + "started": 1762755820632, + "completed": 1762755820961, + "responseAverage": 1.6796666666666666, + "responseMin": 0.404, + "responseMax": 4.039 + }, + "stats": { + "iterations": { + "total": 1, + "failed": 0 + }, + "requests": { + "total": 3, + "failed": 0 + }, + "tests": { + "total": 5, + "failed": 0 + } + }, + "error": null +} diff --git a/packages/insomnia-inso/src/fixtures/run-collection-report/plaintext-report.json b/packages/insomnia-inso/src/fixtures/run-collection-report/plaintext-report.json new file mode 100644 index 0000000000..b1aa62dfaf --- /dev/null +++ b/packages/insomnia-inso/src/fixtures/run-collection-report/plaintext-report.json @@ -0,0 +1,319 @@ +{ + "collection": { + "_id": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1762508027498, + "modified": 1762508027498, + "isPrivate": false, + "description": "", + "metaSortKey": 0, + "type": "Workspace", + "name": "CLI Run Collection Report Generation", + "parentId": "", + "scope": "collection" + }, + "environment": { + "_id": "env_1072af0c15764cd8875f645b0f2e5ad4", + "created": 1754293170062, + "modified": 1762509361180, + "isPrivate": true, + "description": "", + "metaSortKey": 1754293170062, + "type": "Environment", + "color": null, + "data": { + "pri-env-name": "env-value", + "track": 1762509003311, + "track-3": 1762508357019 + }, + "name": "New Environment", + "parentId": "env_6aec164bc7c64ffc96c62e06517c5ba6" + }, + "proxy": { + "proxyEnabled": false, + "httpProxy": "", + "httpsProxy": "", + "noProxy": "" + }, + "executions": [ + { + "request": { + "_id": "req_ef809d54e6aa4d72a54593fdeaa14b93", + "created": 1754468596942, + "modified": 1762509338246, + "isPrivate": false, + "description": "", + "metaSortKey": -1754468596942, + "type": "Request", + "name": "Request With Auth - API Key", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "GET", + "body": { + "mimeType": "multipart/form-data" + }, + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "multipart/form-data" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + } + ], + "authentication": { + "type": "apikey", + "key": "api-key", + "value": "api-key-value", + "disabled": false, + "addTo": "header" + }, + "preRequestScript": "\ninsomnia.expect(200).to.eql(200);\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "\ninsomnia.test('Check if status is 200', () => {\n insomnia.expect(insomnia.response.code).to.eql(200);\n});\n\n", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "263", + "etag": "W/\"107-4RvANbiPgVs3QIOYml7CD9b1VQY\"", + "date": "Mon, 10 Nov 2025 06:23:42 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"GET\",\"headers\":{\"host\":\"localhost:4010\",\"content-type\":\"multipart/form-data; boundary=X-INSOMNIA-BOUNDARY\",\"user-agent\":\"insomnia/11.4.0\",\"api-key\":\"api-key-value\",\"accept\":\"*/*\",\"content-length\":\"25\"},\"data\":\"--X-INSOMNIA-BOUNDARY--\\r\\n\",\"cookies\":{}}", + "responseTime": 3.766 + }, + "tests": [ + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.08066600000006474 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_bb9e621055204a13bc9e301fe376748f", + "created": 1754297299654, + "modified": 1762509360358, + "isPrivate": false, + "description": "", + "metaSortKey": -1754297299654, + "type": "Request", + "name": "Request With Auth - Basic", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "POST", + "body": { + "mimeType": "multipart/form-data", + "params": [ + { + "name": "key", + "value": "value", + "description": "", + "disabled": false + } + ] + }, + "parameters": [ + { + "name": "from-r1", + "value": "{% response 'body', 'req_a7eea21417aa4af2a1c906d5e562a325', 'b64::JC5kYXRh::46b', 'no-history', 60 %}", + "disabled": false + } + ], + "headers": [ + { + "name": "Content-Type", + "value": "multipart/form-data" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + } + ], + "authentication": { + "type": "basic", + "useISO88591": false, + "username": "username", + "password": "password", + "disabled": false + }, + "preRequestScript": "insomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.not.null;\n});\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "374", + "etag": "W/\"176-/xZqOqpzEjCbbk72Hk4zWm66rXM\"", + "date": "Mon, 10 Nov 2025 06:23:42 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"POST\",\"headers\":{\"host\":\"localhost:4010\",\"content-type\":\"multipart/form-data; boundary=X-INSOMNIA-BOUNDARY\",\"user-agent\":\"insomnia/11.4.0\",\"authorization\":\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\",\"accept\":\"*/*\",\"content-length\":\"101\"},\"data\":\"--X-INSOMNIA-BOUNDARY\\r\\nContent-Disposition: form-data; name=\\\"key\\\"\\r\\n\\r\\nvalue\\r\\n--X-INSOMNIA-BOUNDARY--\\r\\n\",\"cookies\":{}}", + "responseTime": 0.426 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.03374999999994088 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_a7eea21417aa4af2a1c906d5e562a325", + "created": 1754293004564, + "modified": 1762509152390, + "isPrivate": false, + "description": "", + "metaSortKey": -1754293004564, + "type": "Request", + "name": "Request With Sensitive Headers", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n\t\"foo\": \"bar\"\n}" + }, + "parameters": [ + { + "name": "queryname", + "value": "queryvalue", + "disabled": false + } + ], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + }, + { + "name": "X-Header", + "value": "foo" + }, + { + "name": "Authorization", + "value": "" + }, + { + "name": "Auth", + "value": "" + }, + { + "name": "Cookie", + "value": "" + }, + { + "name": "x-auth-token", + "value": "" + }, + { + "name": "x-api-key", + "value": "" + } + ], + "authentication": { + "type": "digest", + "disabled": false, + "username": "", + "password": "" + }, + "preRequestScript": "const track = +(new Date());\n\ninsomnia.environment.set(\"track\", track);\n\ninsomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.eql(track);\n});\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "\ninsomnia.test('Check if status is 200', () => {\n insomnia.expect(insomnia.response.code).to.eql(200);\n});\n\ninsomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.not.null;\n});\n", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "348", + "etag": "W/\"15c-nysNeZeBrFfZqNEw3jU0n6zGw2w\"", + "date": "Mon, 10 Nov 2025 06:23:42 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"POST\",\"headers\":{\"host\":\"localhost:4010\",\"auth\":\"\",\"authorization\":\"\",\"content-type\":\"application/json\",\"cookie\":\"\",\"user-agent\":\"insomnia/11.4.0\",\"x-api-key\":\"\",\"x-auth-token\":\"\",\"x-header\":\"foo\",\"accept\":\"*/*\",\"content-length\":\"17\"},\"data\":\"{\\n\\t\\\"foo\\\": \\\"bar\\\"\\n}\",\"cookies\":{}}", + "responseTime": 0.382 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.023083999999926164 + }, + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.03708299999993869 + }, + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.01999999999998181 + } + ], + "iteration": 0 + } + ], + "timing": { + "started": 1762755822497, + "completed": 1762755822776, + "responseAverage": 1.5246666666666666, + "responseMin": 0.382, + "responseMax": 3.766 + }, + "stats": { + "iterations": { + "total": 1, + "failed": 0 + }, + "requests": { + "total": 3, + "failed": 0 + }, + "tests": { + "total": 5, + "failed": 0 + } + }, + "error": null +} diff --git a/packages/insomnia-inso/src/fixtures/run-collection-report/redact-report.json b/packages/insomnia-inso/src/fixtures/run-collection-report/redact-report.json new file mode 100644 index 0000000000..c993255621 --- /dev/null +++ b/packages/insomnia-inso/src/fixtures/run-collection-report/redact-report.json @@ -0,0 +1,319 @@ +{ + "collection": { + "_id": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "created": 1762508027498, + "modified": 1762508027498, + "isPrivate": false, + "description": "", + "metaSortKey": 0, + "type": "Workspace", + "name": "CLI Run Collection Report Generation", + "parentId": "", + "scope": "collection" + }, + "environment": { + "_id": "env_1072af0c15764cd8875f645b0f2e5ad4", + "created": 1754293170062, + "modified": 1762509361180, + "isPrivate": true, + "description": "", + "metaSortKey": 1754293170062, + "type": "Environment", + "color": null, + "name": "New Environment", + "parentId": "env_6aec164bc7c64ffc96c62e06517c5ba6", + "data": { + "pri-env-name": "", + "track": "", + "track-3": "" + } + }, + "proxy": { + "proxyEnabled": false, + "httpProxy": "", + "httpsProxy": "", + "noProxy": "" + }, + "executions": [ + { + "request": { + "_id": "req_ef809d54e6aa4d72a54593fdeaa14b93", + "created": 1754468596942, + "modified": 1762509338246, + "isPrivate": false, + "description": "", + "metaSortKey": -1754468596942, + "type": "Request", + "name": "Request With Auth - API Key", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "GET", + "body": { + "mimeType": "multipart/form-data" + }, + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "multipart/form-data" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + } + ], + "authentication": { + "type": "apikey", + "key": "", + "value": "", + "disabled": false, + "addTo": "" + }, + "preRequestScript": "\ninsomnia.expect(200).to.eql(200);\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "\ninsomnia.test('Check if status is 200', () => {\n insomnia.expect(insomnia.response.code).to.eql(200);\n});\n\n", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "263", + "etag": "W/\"107-4RvANbiPgVs3QIOYml7CD9b1VQY\"", + "date": "Mon, 10 Nov 2025 06:22:52 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"GET\",\"headers\":{\"host\":\"localhost:4010\",\"content-type\":\"multipart/form-data; boundary=X-INSOMNIA-BOUNDARY\",\"user-agent\":\"insomnia/11.4.0\",\"api-key\":\"api-key-value\",\"accept\":\"*/*\",\"content-length\":\"25\"},\"data\":\"--X-INSOMNIA-BOUNDARY--\\r\\n\",\"cookies\":{}}", + "responseTime": 6.6080000000000005 + }, + "tests": [ + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.10637500000007094 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_bb9e621055204a13bc9e301fe376748f", + "created": 1754297299654, + "modified": 1762509360358, + "isPrivate": false, + "description": "", + "metaSortKey": -1754297299654, + "type": "Request", + "name": "Request With Auth - Basic", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "POST", + "body": { + "mimeType": "multipart/form-data", + "params": [ + { + "name": "key", + "value": "value", + "description": "", + "disabled": false + } + ] + }, + "parameters": [ + { + "name": "from-r1", + "value": "{% response 'body', 'req_a7eea21417aa4af2a1c906d5e562a325', 'b64::JC5kYXRh::46b', 'no-history', 60 %}", + "disabled": false + } + ], + "headers": [ + { + "name": "Content-Type", + "value": "multipart/form-data" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + } + ], + "authentication": { + "type": "basic", + "useISO88591": "", + "username": "", + "password": "", + "disabled": false + }, + "preRequestScript": "insomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.not.null;\n});\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "374", + "etag": "W/\"176-/xZqOqpzEjCbbk72Hk4zWm66rXM\"", + "date": "Mon, 10 Nov 2025 06:22:52 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"POST\",\"headers\":{\"host\":\"localhost:4010\",\"content-type\":\"multipart/form-data; boundary=X-INSOMNIA-BOUNDARY\",\"user-agent\":\"insomnia/11.4.0\",\"authorization\":\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\",\"accept\":\"*/*\",\"content-length\":\"101\"},\"data\":\"--X-INSOMNIA-BOUNDARY\\r\\nContent-Disposition: form-data; name=\\\"key\\\"\\r\\n\\r\\nvalue\\r\\n--X-INSOMNIA-BOUNDARY--\\r\\n\",\"cookies\":{}}", + "responseTime": 0.88 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.035499999999956344 + } + ], + "iteration": 0 + }, + { + "request": { + "_id": "req_a7eea21417aa4af2a1c906d5e562a325", + "created": 1754293004564, + "modified": 1762509152390, + "isPrivate": false, + "description": "", + "metaSortKey": -1754293004564, + "type": "Request", + "name": "Request With Sensitive Headers", + "parentId": "wrk_c5d5b5167b9049af8b53c5b6f076349b", + "url": "http://localhost:4010/echo", + "method": "POST", + "body": { + "mimeType": "application/json", + "text": "{\n\t\"foo\": \"bar\"\n}" + }, + "parameters": [ + { + "name": "queryname", + "value": "queryvalue", + "disabled": false + } + ], + "headers": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "User-Agent", + "value": "insomnia/11.4.0" + }, + { + "name": "X-Header", + "value": "foo" + }, + { + "name": "Authorization", + "value": "" + }, + { + "name": "Auth", + "value": "" + }, + { + "name": "Cookie", + "value": "" + }, + { + "name": "x-auth-token", + "value": "" + }, + { + "name": "x-api-key", + "value": "" + } + ], + "authentication": { + "type": "digest", + "disabled": false, + "username": "", + "password": "" + }, + "preRequestScript": "const track = +(new Date());\n\ninsomnia.environment.set(\"track\", track);\n\ninsomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.eql(track);\n});\n", + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingFollowRedirects": "global", + "settingSendCookies": true, + "settingStoreCookies": true, + "settingRebuildPath": true, + "afterResponseScript": "\ninsomnia.test('Check if status is 200', () => {\n insomnia.expect(insomnia.response.code).to.eql(200);\n});\n\ninsomnia.test('Check if track exists', () => {\n insomnia.expect(insomnia.environment.get(\"track\")).to.not.null;\n});\n", + "pathParameters": [] + }, + "response": { + "status": "OK", + "code": 200, + "headers": { + "x-powered-by": "Express", + "content-type": "application/json; charset=utf-8", + "content-length": "348", + "etag": "W/\"15c-nysNeZeBrFfZqNEw3jU0n6zGw2w\"", + "date": "Mon, 10 Nov 2025 06:22:52 GMT", + "connection": "keep-alive", + "keep-alive": "timeout=5" + }, + "data": "{\"method\":\"POST\",\"headers\":{\"host\":\"localhost:4010\",\"auth\":\"\",\"authorization\":\"\",\"content-type\":\"application/json\",\"cookie\":\"\",\"user-agent\":\"insomnia/11.4.0\",\"x-api-key\":\"\",\"x-auth-token\":\"\",\"x-header\":\"foo\",\"accept\":\"*/*\",\"content-length\":\"17\"},\"data\":\"{\\n\\t\\\"foo\\\": \\\"bar\\\"\\n}\",\"cookies\":{}}", + "responseTime": 0.575 + }, + "tests": [ + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.027999999999792635 + }, + { + "testCase": "Check if status is 200", + "status": "passed", + "executionTime": 0.03275000000007822 + }, + { + "testCase": "Check if track exists", + "status": "passed", + "executionTime": 0.018499999999903594 + } + ], + "iteration": 0 + } + ], + "timing": { + "started": 1762755772438, + "completed": 1762755772769, + "responseAverage": 2.687666666666667, + "responseMin": 0.575, + "responseMax": 6.6080000000000005 + }, + "stats": { + "iterations": { + "total": 1, + "failed": 0 + }, + "requests": { + "total": 3, + "failed": 0 + }, + "tests": { + "total": 5, + "failed": 0 + } + }, + "error": null +} diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index 96ae58ec97..b3e0362d19 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -130,9 +130,9 @@ export async function getSendRequestCallbackMemDb( } const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res; - const headers = headerArray?.reduce( + const headers = headerArray?.reduce>( (acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), - [], + {}, ); const bodyBuffer = (await getBodyBuffer(res)) as Buffer; const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;