diff --git a/package-lock.json b/package-lock.json index 17ca4c9ef9b..840816d99c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5735,8 +5735,7 @@ "node_modules/@types/deep-equal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/deep-equal/-/deep-equal-1.0.4.tgz", - "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==", - "dev": true + "integrity": "sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==" }, "node_modules/@types/dompurify": { "version": "3.0.5", @@ -10360,7 +10359,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -13326,7 +13324,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13439,7 +13436,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13530,7 +13526,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13553,7 +13548,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "get-intrinsic": "^1.2.4" @@ -17027,7 +17021,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" @@ -19941,7 +19934,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -22065,7 +22057,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -23077,9 +23068,10 @@ } }, "packages/insomnia-sdk": { - "version": "9.0.0", + "version": "0.1.0", "license": "Apache-2.0", "dependencies": { + "@types/deep-equal": "^1.0.4", "@types/tv4": "^1.2.33", "@types/xml2js": "^0.4.14", "ajv": "^8.12.0", @@ -23087,6 +23079,7 @@ "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "csv-parse": "^5.5.5", + "deep-equal": "^2.2.3", "lodash": "^4.17.21", "moment": "^2.30.1", "tv4": "^1.3.0", @@ -23133,6 +23126,37 @@ "node": ">=6" } }, + "packages/insomnia-sdk/node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "packages/insomnia-sdk/node_modules/loupe": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", diff --git a/packages/insomnia-sdk/package.json b/packages/insomnia-sdk/package.json index c80a3d6854a..b588a589de1 100644 --- a/packages/insomnia-sdk/package.json +++ b/packages/insomnia-sdk/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "insomnia-sdk", - "version": "9.0.0", + "version": "0.1.0", "description": "", "main": "src/objects/index.ts", "types": "src/objects/index.ts", @@ -20,6 +20,7 @@ }, "homepage": "https://github.com/Kong/insomnia#readme", "dependencies": { + "@types/deep-equal": "^1.0.4", "@types/tv4": "^1.2.33", "@types/xml2js": "^0.4.14", "ajv": "^8.12.0", @@ -27,6 +28,7 @@ "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "csv-parse": "^5.5.5", + "deep-equal": "^2.2.3", "lodash": "^4.17.21", "moment": "^2.30.1", "tv4": "^1.3.0", diff --git a/packages/insomnia-sdk/src/objects/__tests__/response.test.ts b/packages/insomnia-sdk/src/objects/__tests__/response.test.ts index 301954a0bff..4e97eab49c0 100644 --- a/packages/insomnia-sdk/src/objects/__tests__/response.test.ts +++ b/packages/insomnia-sdk/src/objects/__tests__/response.test.ts @@ -62,5 +62,12 @@ describe('test request and response objects', () => { mimeFormat: '', mimeType: 'text/plain', }); + + // extended assertion chains + resp.to.have.status(200); + resp.to.have.status('OK'); + resp.to.have.header('header1'); + resp.to.have.jsonBody({ 'key': 888 }); + resp.to.have.body('{"key": 888}'); }); }); diff --git a/packages/insomnia-sdk/src/objects/insomnia.ts b/packages/insomnia-sdk/src/objects/insomnia.ts index cc56f7e8977..ef7b0976d32 100644 --- a/packages/insomnia-sdk/src/objects/insomnia.ts +++ b/packages/insomnia-sdk/src/objects/insomnia.ts @@ -11,6 +11,7 @@ import { unsupportedError } from './properties'; import { Request as ScriptRequest, RequestOptions, toScriptRequestBody } from './request'; import { RequestInfo } from './request-info'; import { Response as ScriptResponse } from './response'; +import { readBodyFromPath, toScriptResponse } from './response'; import { sendRequest } from './send-request'; import { test } from './test'; import { toUrlObject } from './urls'; @@ -23,6 +24,7 @@ export class InsomniaObject { public request: ScriptRequest; public cookies: CookieObject; public info: RequestInfo; + public response?: ScriptResponse; private clientCertificates: ClientCertificate[]; private _expect = expect; @@ -47,6 +49,7 @@ export class InsomniaObject { clientCertificates: ClientCertificate[]; cookies: CookieObject; requestInfo: RequestInfo; + response?: ScriptResponse; }, log: (...msgs: any[]) => void, ) { @@ -57,6 +60,7 @@ export class InsomniaObject { this._iterationData = rawObj.iterationData; this.variables = rawObj.variables; this.cookies = rawObj.cookies; + this.response = rawObj.response; this.info = rawObj.requestInfo; this.request = rawObj.request; @@ -108,11 +112,12 @@ export class InsomniaObject { clientCertificates: this.clientCertificates, cookieJar: this.cookies.jar().toInsomniaCookieJar(), info: this.info.toObject(), + response: this.response ? this.response.toObject() : undefined, }; }; } -export function initInsomniaObject( +export async function initInsomniaObject( rawObj: RequestContext, log: (...args: any[]) => void, ) { @@ -206,6 +211,9 @@ export function initInsomniaObject( }; const request = new ScriptRequest(reqOpt); + const responseBody = await readBodyFromPath(rawObj.response); + const response = rawObj.response ? toScriptResponse(request, rawObj.response, responseBody) : undefined; + return new InsomniaObject( { globals, @@ -218,6 +226,7 @@ export function initInsomniaObject( clientCertificates: rawObj.clientCertificates, cookies, requestInfo, + response, }, log, ); diff --git a/packages/insomnia-sdk/src/objects/interfaces.ts b/packages/insomnia-sdk/src/objects/interfaces.ts index badb395a84c..bf974553883 100644 --- a/packages/insomnia-sdk/src/objects/interfaces.ts +++ b/packages/insomnia-sdk/src/objects/interfaces.ts @@ -2,6 +2,7 @@ import { CookieJar as InsomniaCookieJar } from 'insomnia/src//models/cookie-jar' import { ClientCertificate } from 'insomnia/src/models/client-certificate'; import type { Request } from 'insomnia/src/models/request'; import { Settings } from 'insomnia/src/models/settings'; +import { sendCurlAndWriteTimelineError, sendCurlAndWriteTimelineResponse } from 'insomnia/src/network/network'; export interface RequestContext { request: Request; @@ -17,4 +18,6 @@ export interface RequestContext { settings: Settings; clientCertificates: ClientCertificate[]; cookieJar: InsomniaCookieJar; + // only for the post-request script + response?: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError; } diff --git a/packages/insomnia-sdk/src/objects/request.ts b/packages/insomnia-sdk/src/objects/request.ts index 6f7a28b9bfd..ee503ef949c 100644 --- a/packages/insomnia-sdk/src/objects/request.ts +++ b/packages/insomnia-sdk/src/objects/request.ts @@ -593,7 +593,7 @@ export function mergeRequestBody( try { const textContent = updatedReqBody?.raw ? updatedReqBody?.raw : - updatedReqBody?.graphql ? JSON.stringify(updatedReqBody?.graphql) : ''; + updatedReqBody?.graphql ? JSON.stringify(updatedReqBody?.graphql) : undefined; return { mimeType: mimeType, diff --git a/packages/insomnia-sdk/src/objects/response.ts b/packages/insomnia-sdk/src/objects/response.ts index 877be3f093b..bf724bbd958 100644 --- a/packages/insomnia-sdk/src/objects/response.ts +++ b/packages/insomnia-sdk/src/objects/response.ts @@ -1,4 +1,6 @@ +import deepEqual from 'deep-equal'; import { RESPONSE_CODE_REASONS } from 'insomnia/src/common/constants'; +import { sendCurlAndWriteTimelineError, type sendCurlAndWriteTimelineResponse } from 'insomnia/src/network/network'; import { Cookie, CookieOptions } from './cookies'; import { CookieList } from './cookies'; @@ -15,7 +17,6 @@ export interface ResponseOptions { // ideally it should work in both browser and node stream?: Buffer | ArrayBuffer; responseTime: number; - status?: string; originalRequest: Request; } @@ -57,7 +58,7 @@ export class Response extends Property { ); this.originalRequest = options.originalRequest; this.responseTime = options.responseTime; - this.status = RESPONSE_CODE_REASONS[options.code]; + this.status = options.reason || RESPONSE_CODE_REASONS[options.code]; this.stream = options.stream; } @@ -80,7 +81,7 @@ export class Response extends Property { stream: response.stream, header: response.headers, code: response.statusCode, - status: response.statusMessage, + reason: response.statusMessage, responseTime: response.elapsedTime, originalRequest: response.originalRequest, }); @@ -155,7 +156,7 @@ export class Response extends Property { try { return JSON.parse(this.body.toString(), reviver); } catch (e) { - throw Error(`json: faile to parse: ${e}`); + throw Error(`json: failed to parse: ${e}`); } } @@ -182,4 +183,104 @@ export class Response extends Property { text() { return this.body.toString(); } + + // Besides chai.expect, "to" is extended to support cases like: + // insomnia.response.to.have.status(200); + get to() { + type valueType = boolean | number | string | object | undefined; + const verify = (got: valueType, expected: valueType) => { + if (['boolean', 'number', 'string', 'undefined'].includes(typeof got) && expected === got) { + return; + } else if (deepEqual(got, expected, { strict: true })) { + return; + } + throw Error(`"${got}" is not equal to the expected value: "${expected}"`); + }; + + return { + // follows extend chai's chains for compatibility + have: { + status: (expected: number | string) => { + if (typeof expected === 'string') { + verify(this.status, expected); + } else { + verify(this.code, expected); + } + }, + header: (expected: string) => verify( + this.headers.toObject().find(header => header.key === expected) !== undefined, + true, + ), + + body: (expected: string) => verify(this.text(), expected), + jsonBody: (expected: object) => verify(this.json(), expected), + }, + }; + } +} + +export function toScriptResponse( + originalRequest: Request, + partialInsoResponse: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError, + responseBody: string, +): Response | undefined { + if ('error' in partialInsoResponse) { + // it is sendCurlAndWriteTimelineError and basically doesn't contain anything useful + return undefined; + } + const partialResponse = partialInsoResponse as sendCurlAndWriteTimelineResponse; + + const headers = partialResponse.headers ? + partialResponse.headers.map( + insoHeader => ({ + key: insoHeader.name, + value: insoHeader.value, + }), + {}, + ) + : []; + + const insoCookieOptions = partialResponse.headers ? + partialResponse.headers + .filter( + header => { + return header.name.toLowerCase() === 'set-cookie'; + }, + {}, + ).map( + setCookieHeader => Cookie.parse(setCookieHeader.value) + ) + : []; + + const responseOption = { + code: partialResponse.statusCode || 0, + reason: partialResponse.statusMessage, + header: headers, + cookie: insoCookieOptions, + body: responseBody, + // stream is duplicated with body + responseTime: partialResponse.elapsedTime, + originalRequest, + }; + + return new Response(responseOption); +}; + +export async function readBodyFromPath(response: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError | undefined) { + // it allows to execute scripts (e.g., for testing) but body contains nothing + if (!response || 'error' in response) { + return ''; + } else if (!response.bodyPath) { + return ''; + } + + const readResponseResult = await window.bridge.readCurlResponse({ + bodyPath: response.bodyPath, + bodyCompression: response.bodyCompression, + }); + + if (readResponseResult.error) { + throw Error(`Failed to read body: ${readResponseResult.error}`); + } + return readResponseResult.body; } diff --git a/packages/insomnia-sdk/src/objects/send-request.ts b/packages/insomnia-sdk/src/objects/send-request.ts index b23c0741056..dba3bc310ea 100644 --- a/packages/insomnia-sdk/src/objects/send-request.ts +++ b/packages/insomnia-sdk/src/objects/send-request.ts @@ -244,7 +244,6 @@ async function curlOutputToResponse( body: '', stream: undefined, responseTime: result.patch.elapsedTime, - status: lastRedirect.reason, originalRequest, }); } @@ -266,7 +265,6 @@ async function curlOutputToResponse( // because it is inaccurate to differentiate if body is binary stream: undefined, responseTime: result.patch.elapsedTime, - status: lastRedirect.reason, originalRequest, }); } diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts index 5a16b160a0d..8097584be23 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts @@ -48,7 +48,7 @@ test('handle hidden browser window getting closed', async ({ app, page }) => { await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter'); await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); - await page.getByText('Timeout: Pre-request script took too long').click(); + await page.getByText('Timeout: Running script took too long').click(); await page.getByRole('tab', { name: 'Timeline' }).click(); await page.getByRole('tab', { name: 'Preview ' }).click(); const windows = await app.windows(); @@ -56,5 +56,5 @@ test('handle hidden browser window getting closed', async ({ app, page }) => { hiddenWindow.close(); await page.getByRole('button', { name: 'Send' }).click(); // as the hidden window is restarted, it should not show "Timeout: Hidden browser window is not responding" - await page.getByText('Timeout: Pre-request script took too long').click(); + await page.getByText('Timeout: Running script took too long').click(); }); diff --git a/packages/insomnia/src/hidden-window-preload.ts b/packages/insomnia/src/hidden-window-preload.ts index 5f60c08800f..e7654a674c9 100644 --- a/packages/insomnia/src/hidden-window-preload.ts +++ b/packages/insomnia/src/hidden-window-preload.ts @@ -34,7 +34,7 @@ export interface HiddenBrowserWindowToMainBridgeAPI { curlRequest: (options: any) => Promise; readCurlResponse: (options: { bodyPath: string; bodyCompression: Compression }) => Promise<{ body: string; error: string }>; setBusy: (busy: boolean) => void; - writeFile: (logPath: string, logContent: string) => Promise; + appendFile: (logPath: string, logContent: string) => Promise; asyncTasksAllSettled: () => Promise; resetAsyncTasks: () => void; stopMonitorAsyncTasks: () => void; @@ -115,8 +115,8 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = { setBusy: busy => ipcRenderer.send('set-hidden-window-busy-status', busy), // TODO: following methods are for simulating current behavior of running async tasks // in the future, it should be better to keep standard way of handling async tasks to avoid confusion - writeFile: (logPath: string, logContent: string) => { - return fs.promises.writeFile(logPath, logContent); + appendFile: (logPath: string, logContent: string) => { + return fs.promises.appendFile(logPath, logContent); }, Promise: OriginalPromise, asyncTasksAllSettled, diff --git a/packages/insomnia/src/hidden-window.ts b/packages/insomnia/src/hidden-window.ts index 808d0af7815..bd350c624ca 100644 --- a/packages/insomnia/src/hidden-window.ts +++ b/packages/insomnia/src/hidden-window.ts @@ -5,7 +5,7 @@ import * as _ from 'lodash'; import { invariant } from '../src/utils/invariant'; export interface HiddenBrowserWindowBridgeAPI { - runPreRequestScript: (options: { script: string; context: RequestContext }) => Promise; + runScript: (options: { script: string; context: RequestContext }) => Promise; }; window.bridge.onmessage(async (data, callback) => { @@ -15,10 +15,10 @@ window.bridge.onmessage(async (data, callback) => { const timeout = data.context.timeout || 5000; const timeoutPromise = new window.bridge.Promise(resolve => { setTimeout(() => { - resolve({ error: 'Timeout: Pre-request script took too long' }); + resolve({ error: 'Timeout: Running script took too long' }); }, timeout); }); - const result = await window.bridge.Promise.race([timeoutPromise, runPreRequestScript(data)]); + const result = await window.bridge.Promise.race([timeoutPromise, runScript(data)]); callback(result); } catch (err) { console.error('error', err); @@ -28,13 +28,13 @@ window.bridge.onmessage(async (data, callback) => { } }); -const runPreRequestScript = async ( +const runScript = async ( { script, context }: { script: string; context: RequestContext }, ): Promise => { console.log(script); const scriptConsole = new Console(); - const executionContext = initInsomniaObject(context, scriptConsole.log); + const executionContext = await initInsomniaObject(context, scriptConsole.log); const evalInterceptor = (script: string) => { invariant(script && typeof script === 'string', 'eval is called with invalid or empty value'); @@ -83,7 +83,7 @@ const runPreRequestScript = async ( const updatedCertificates = mergeClientCertificates(context.clientCertificates, mutatedContextObject.request); const updatedCookieJar = mergeCookieJar(context.cookieJar, mutatedContextObject.cookieJar); - await window.bridge.writeFile(context.timelinePath, scriptConsole.dumpLogs()); + await window.bridge.appendFile(context.timelinePath, scriptConsole.dumpLogs()); console.log('mutatedInsomniaObject', mutatedContextObject); console.log('context', context); diff --git a/packages/insomnia/src/network/cancellation.ts b/packages/insomnia/src/network/cancellation.ts index 8f0f0e7318b..ebcba8ede09 100644 --- a/packages/insomnia/src/network/cancellation.ts +++ b/packages/insomnia/src/network/cancellation.ts @@ -14,7 +14,7 @@ export async function cancelRequestById(requestId: string) { console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`); } -export const cancellableRunPreRequestScript = async (options: { script: string; context: RequestContext }) => { +export const cancellableRunScript = async (options: { script: string; context: RequestContext }) => { const request = options.context.request; const requestId = request._id; @@ -28,7 +28,7 @@ export const cancellableRunPreRequestScript = async (options: { script: string; try { const result = await cancellablePromise({ signal: controller.signal, - fn: window.main.hiddenBrowserWindow.runPreRequestScript(options), + fn: window.main.hiddenBrowserWindow.runScript(options), }); return result as { diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index ffc3871c01e..7f70db6adf1 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -34,7 +34,7 @@ import { smartEncodeUrl, } from '../utils/url/querystring'; import { getAuthHeader, getAuthQueryParams } from './authentication'; -import { cancellableCurlRequest, cancellableRunPreRequestScript } from './cancellation'; +import { cancellableCurlRequest, cancellableRunScript } from './cancellation'; import { filterClientCertificates } from './certificate'; import { addSetCookiesToToughCookieJar } from './set-cookie-util'; @@ -73,22 +73,16 @@ export const fetchRequestData = async (requestId: string) => { return { request, environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId }; }; -export const tryToExecutePreRequestScript = async ( - request: Request, - environment: Environment, - timelinePath: string, - responseId: string, - baseEnvironment: Environment, - clientCertificates: ClientCertificate[], - cookieJar: CookieJar, -) => { - if (!request.preRequestScript) { +export const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse) => { + const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response } = context; + if (!script) { return { request, environment: undefined, baseEnvironment: undefined, }; } + const settings = await models.settings.get(); try { @@ -100,8 +94,8 @@ export const tryToExecutePreRequestScript = async ( // TODO: restart the hidden browser window }, timeout + 1000); }); - const preRequestPromise = cancellableRunPreRequestScript({ - script: request.preRequestScript, + const executionPromise = cancellableRunScript({ + script, context: { request, timelinePath, @@ -115,9 +109,10 @@ export const tryToExecutePreRequestScript = async ( clientCertificates, settings, cookieJar, + response, }, }); - const output = await Promise.race([timeoutPromise, preRequestPromise]) as { + const output = await Promise.race([timeoutPromise, executionPromise]) as { request: Request; environment: Record; baseEnvironment: Record; @@ -125,7 +120,7 @@ export const tryToExecutePreRequestScript = async ( clientCertificates: ClientCertificate[]; cookieJar: CookieJar; }; - console.log('[network] Pre-request script succeeded', output); + console.log('[network] script execution succeeded', output); const envPropertyOrder = orderedJSON.parse( JSON.stringify(output.environment), @@ -152,7 +147,10 @@ export const tryToExecutePreRequestScript = async ( cookieJar: output.cookieJar, }; } catch (err) { - await fs.promises.appendFile(timelinePath, JSON.stringify({ value: err.message, name: 'Text', timestamp: Date.now() }) + '\n'); + await fs.promises.appendFile( + timelinePath, + JSON.stringify({ value: err.message, name: 'Text', timestamp: Date.now() }) + '\n', + ); const requestId = request._id; const responsePatch = { @@ -169,6 +167,30 @@ export const tryToExecutePreRequestScript = async ( } }; +interface RequestContextForScript { + request: Request; + environment: Environment; + timelinePath: string; + responseId: string; + baseEnvironment: Environment; + clientCertificates: ClientCertificate[]; + cookieJar: CookieJar; +} +type RequestAndContextAndResponse = RequestContextForScript & { + response: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse; +}; +type RequestAndContextAndOptionalResponse = RequestContextForScript & { + script: string; + response?: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse; +}; +export async function tryToExecutePreRequestScript(context: RequestContextForScript) { + return tryToExecuteScript({ script: context.request.preRequestScript, ...context }); +}; + +export async function tryToExecutePostRequestScript(context: RequestAndContextAndResponse) { + return tryToExecuteScript({ script: context.request.postRequestScript, ...context }); +} + export const tryToInterpolateRequest = async ( request: Request, environment: string | Environment, @@ -202,6 +224,26 @@ export const tryToTransformRequestWithPlugins = async (renderResult: RequestAndC throw new Error(`Failed to transform request with plugins: ${request._id}`); } }; + +export interface sendCurlAndWriteTimelineError { + _id: string; + parentId: string; + timelinePath: string; + statusMessage: string; + // additional + url: string; + error: string; + elapsedTime: number; + bytesRead: number; +} + +export interface sendCurlAndWriteTimelineResponse extends ResponsePatch { + _id: string; + parentId: string; + timelinePath: string; + statusMessage: string; +} + export async function sendCurlAndWriteTimeline( renderedRequest: RenderedRequest, clientCertificates: ClientCertificate[], @@ -209,7 +251,7 @@ export async function sendCurlAndWriteTimeline( settings: Settings, timelinePath: string, responseId: string, -) { +): Promise { const requestId = renderedRequest._id; const timelineStrings: string[] = []; const authentication = renderedRequest.authentication as RequestAuthentication; @@ -282,6 +324,7 @@ export async function sendCurlAndWriteTimeline( ...patch, }; } + export const responseTransform = async (patch: ResponsePatch, environmentId: string | null, renderedRequest: RenderedRequest, context: Record) => { const response: ResponsePatch = { ...patch, diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index aaf33b489ee..b167cdd6e5d 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -73,7 +73,7 @@ const main: Window['main'] = { }, }, hiddenBrowserWindow: { - runPreRequestScript: options => new Promise(async (resolve, reject) => { + runScript: options => new Promise(async (resolve, reject) => { await ipcRenderer.invoke('open-channel-to-hidden-browser-window'); const port = ports.get('hiddenWindowPort'); diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index ec515287a01..eecf021d952 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -58,6 +58,18 @@ const updateRequestAuth = );`; const requireAModule = "const atob = require('atob');"; +const getStatusCode = 'const statusCode = insomnia.response.code;'; +const getStatusMsg = 'const status = insomnia.response.status;'; +const getRespTime = 'const responseTime = insomnia.response.responseTime;'; +const getJsonBody = 'const jsonBody = insomnia.response.json();'; +const getTextBody = 'const textBody = insomnia.response.text();'; +const findHeader = + `const header = insomnia.response.headers.find( + header => header.key === 'Content-Type', + {}, +);`; +const getCookies = 'const cookies = insomnia.response.cookies.toObject();'; + const lintOptions = { globals: { // https://jshint.com/docs/options/ @@ -380,6 +392,70 @@ export const RequestScriptEditor: FC = ({ /> + + + + + } + > + + addSnippet(getStatusCode)} + /> + + + addSnippet(getStatusMsg)} + /> + + + addSnippet(getRespTime)} + /> + + + addSnippet(getJsonBody)} + /> + + + addSnippet(getTextBody)} + /> + + + addSnippet(findHeader)} + /> + + + addSnippet(getCookies)} + /> + + + { try { const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams; - const mutatedContext = await tryToExecutePreRequestScript( - req, + const mutatedContext = await tryToExecutePreRequestScript({ + request: req, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, - ); + }); if (!mutatedContext?.request) { // exiy early if there was a problem with the pre-request script // TODO: improve error message? return null; } else { - // persist updated cookieJar if needed - if (mutatedContext.cookieJar) { - await models.cookieJar.update( - mutatedContext.cookieJar, - { cookies: mutatedContext.cookieJar.cookies }, - ); - } - // when base environment is activated, `mutatedContext.environment` points to it - const isActiveEnvironmentBase = mutatedContext.environment?._id === baseEnvironment._id; - const hasEnvironmentAndIsNotBase = mutatedContext.environment && !isActiveEnvironmentBase; - if (hasEnvironmentAndIsNotBase) { - await models.environment.update( - environment, - { - data: mutatedContext.environment.data, - dataPropertyOrder: mutatedContext.environment.dataPropertyOrder, - } - ); - } - if (mutatedContext.baseEnvironment) { - await models.environment.update( - baseEnvironment, - { - data: mutatedContext.baseEnvironment.data, - dataPropertyOrder: mutatedContext.baseEnvironment.dataPropertyOrder, - } - ); - } + await savePatchesMadeByScript(mutatedContext, environment, baseEnvironment); } const renderedResult = await tryToInterpolateRequest( @@ -461,12 +435,32 @@ export const sendAction: ActionFunction = async ({ request, params }) => { const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, renderedResult.context); const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath; const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath; + + const postMutatedContext = await tryToExecutePostRequestScript({ + request: req, + environment, + timelinePath, + responseId, + baseEnvironment, + clientCertificates, + cookieJar, + response, + }); + if (!postMutatedContext?.request) { + // exiy early if there was a problem with the pre-request script + // TODO: improve error message? + return null; + } else { + await savePatchesMadeByScript(postMutatedContext, environment, baseEnvironment); + } + if (!shouldWriteToFile) { const response = await models.response.create(responsePatch, settings.maxHistoryResponses); await models.requestMeta.update(requestMeta, { activeResponseId: response._id }); // setLoading(false); return null; } + if (requestMeta.downloadPath) { const header = getContentDispositionHeader(responsePatch.headers || []); const name = header @@ -500,6 +494,45 @@ export const sendAction: ActionFunction = async ({ request, params }) => { } }; +async function savePatchesMadeByScript( + mutatedContext: Awaited>, + environment: Environment, + baseEnvironment: Environment, +) { + if (!mutatedContext) { + return; + } + + // persist updated cookieJar if needed + if (mutatedContext.cookieJar) { + await models.cookieJar.update( + mutatedContext.cookieJar, + { cookies: mutatedContext.cookieJar.cookies }, + ); + } + // when base environment is activated, `mutatedContext.environment` points to it + const isActiveEnvironmentBase = mutatedContext.environment?._id === baseEnvironment._id; + const hasEnvironmentAndIsNotBase = mutatedContext.environment && !isActiveEnvironmentBase; + if (hasEnvironmentAndIsNotBase) { + await models.environment.update( + environment, + { + data: mutatedContext.environment.data, + dataPropertyOrder: mutatedContext.environment.dataPropertyOrder, + } + ); + } + if (mutatedContext.baseEnvironment) { + await models.environment.update( + baseEnvironment, + { + data: mutatedContext.baseEnvironment.data, + dataPropertyOrder: mutatedContext.baseEnvironment.dataPropertyOrder, + } + ); + } +} + export const createAndSendToMockbinAction: ActionFunction = async ({ request }) => { const patch = await request.json() as Partial; invariant(typeof patch.url === 'string', 'URL is required');