feat: enable major features of the after-response script (#7411)

* feat: update data model and request-pane to support post-req-script

* fix: unit test failed

* feat: integrate post-request script to the engine - INS-3785,INS-3786 (#7329)

* feat: integrate post-request script to the engine

* refactor: some minor improvements

* fix: lint error

* chore: clean up typings

* refactor: separate transforming into sync and async parts

* use named args

* fix: renaming pre-req vars, functions and 2 minor fixes

* fix: the error message is updated

* feat: add snippets for post-request scripting (#7395)

* feat: enable extended assertion chains on `insomnia.response` (#7396)

* feat: add snippets for post-request scripting

* feat(sdk): support response.to.have assertion for verifying response

* chore: fix lint error

---------

Co-authored-by: jackkav <jackkav@gmail.com>

* test: add tests for post-request scripts and post-request scripts - INS-3786 (#7331)

* test: add some tests for post-req script and script engine

* fix: incorrect script type

* chore: refresh package-lock after merging

* chore: clean up package-lock.json

* fix: failed tests after rebasing

* feat: support importing post-req script from Postman (#7423)

* feat: support importing post-req script from Postman

* fix: introduce post-req script property for merged changes

* test: add a test case for importing scripts

* fix: add missing fixture

* chore: remove row after merging

* chore: rename to after-response-script

* fix test

* refresh lock

* update snapshot

* extract pre request logic to function

* refresh lock again

* throw on base env

* fix: revert the logic which rejects the case of unselected environment

---------

Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
Hexxa
2024-05-23 15:25:35 +08:00
committed by GitHub
parent 2d872ab881
commit 46b6ea811e
29 changed files with 842 additions and 130 deletions

44
package-lock.json generated
View File

@@ -5837,8 +5837,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",
@@ -10470,7 +10469,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",
@@ -13438,7 +13436,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"
},
@@ -13551,7 +13548,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"
},
@@ -13642,7 +13638,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"
},
@@ -13665,7 +13660,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"
@@ -17174,7 +17168,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"
@@ -20114,7 +20107,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"
},
@@ -22243,7 +22235,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",
@@ -23265,6 +23256,7 @@
"version": "9.2.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",
@@ -23272,6 +23264,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",
@@ -23318,6 +23311,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",

View File

@@ -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",

View File

@@ -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}');
});
});

View File

@@ -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,
);

View File

@@ -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 after-response script
response?: sendCurlAndWriteTimelineResponse | sendCurlAndWriteTimelineError;
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,
});
}

View File

@@ -0,0 +1,109 @@
_type: export
__export_format: 4
__export_date: 2024-02-13T07:27:17.322Z
__export_source: insomnia.desktop.app:v8.6.1
resources:
- _id: wrk_6b9b8455fd784462ae19cd51d7156f86
parentId: null
modified: 1707808692801
created: 1707808692801
name: After-response Scripts
description: ""
scope: collection
_type: workspace
- _id: req_244fe815da6c4342a17f0cfd98cf648c
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
modified: 1707809218855
created: 1707808697304
url: http://127.0.0.1:4010/echo
name: tests with expect and test
description: ""
method: POST
afterResponseScript: |-
insomnia.test('happy tests', () => {
insomnia.expect(200).to.eql(200);
insomnia.expect('uname').to.be.a('string');
insomnia.expect('a').to.have.lengthOf(1);
insomnia.expect('xxx_customer_id_yyy').to.include("customer_id");
insomnia.expect(201).to.be.oneOf([201,202]);
insomnia.expect(199).to.be.below(200);
// test objects
insomnia.expect({a: 1, b: 2}).to.have.all.keys('a', 'b');
insomnia.expect({a: 1, b: 2}).to.have.any.keys('a', 'b');
insomnia.expect({a: 1, b: 2}).to.not.have.any.keys('c', 'd');
insomnia.expect({a: 1}).to.have.property('a');
insomnia.expect({a: 1, b: 2}).to.be.a('object')
.that.has.all.keys('a', 'b');
});
insomnia.test('unhappy tests', () => {
insomnia.expect(199).to.eql(200);
insomnia.expect(199).to.be.oneOf([201,202]);
});
body:
mimeType: "application/json"
text: |-
{}
parameters: []
headers:
- name: 'Content-Type'
value: 'application/json'
authentication: {}
metaSortKey: -1707809028499
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_244fe815da6c4342a17f0cfd98cf6401
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
modified: 1707809218855
created: 1707808697304
url: http://127.0.0.1:4010/echo
name: persist environments
description: ""
method: POST
afterResponseScript: |-
insomnia.environment.set('__fromAfterScript', 'environment');
insomnia.baseEnvironment.set('__fromAfterScript1', 'baseEnvironment');
insomnia.collectionVariables.set('__fromAfterScript2', 'collection');
body:
mimeType: "application/json"
text: |-
{}
parameters: []
headers:
- name: 'Content-Type'
value: 'application/json'
authentication: {}
metaSortKey: -1707809028499
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: env_f9ef1d097c5e00986051fcb4f7a921eea1a86916
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
modified: 1707808692805
created: 1707808692805
name: Base Environment
data: {}
dataPropertyOrder: null
color: null
isPrivate: false
metaSortKey: 1707808692805
_type: environment
- _id: jar_f9ef1d097c5e00986051fcb4f7a921eea1a86916
parentId: wrk_6b9b8455fd784462ae19cd51d7156f86
modified: 1707808692807
created: 1707808692807
name: Default Jar
cookies: []
_type: cookie_jar

View File

@@ -0,0 +1,60 @@
import { expect } from '@playwright/test';
import { loadFixture } from '../../playwright/paths';
import { test } from '../../playwright/test';;
test.describe('after-response script features tests', async () => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
test.beforeEach(async ({ app, page }) => {
const text = await loadFixture('after-response-collection.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
await page.getByRole('button', { name: 'Create in project' }).click();
await page.getByRole('menuitemradio', { name: 'Import' }).click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
await page.getByLabel('After-response Scripts').click();
});
test('insomnia.test and insomnia.expect can work together', async ({ page }) => {
const responsePane = page.getByTestId('response-pane');
await page.getByLabel('Request Collection').getByTestId('tests with expect and test').press('Enter');
// send
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify
await page.getByRole('tab', { name: 'Timeline' }).click();
await expect(responsePane).toContainText('✓ happy tests');
await expect(responsePane).toContainText('✕ unhappy tests: AssertionError: expected 199 to deeply equal 200');
});
test('environment and baseEnvironment can be persisted', async ({ page }) => {
const statusTag = page.locator('[data-testid="response-status-tag"]:visible');
await page.getByLabel('Request Collection').getByTestId('persist environments').press('Enter');
// send
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify response
await page.waitForSelector('[data-testid="response-status-tag"]:visible');
await expect(statusTag).toContainText('200 OK');
// verify persisted environment
await page.getByLabel('Manage Environments').click();
const responseBody = page.getByRole('dialog').getByTestId('CodeEditor').locator('.CodeMirror-line');
const rows = await responseBody.allInnerTexts();
const bodyJson = JSON.parse(rows.join(' '));
expect(bodyJson).toEqual({
// no environment is selected so the environment value is not persisted
'__fromAfterScript1': 'baseEnvironment',
'__fromAfterScript2': 'collection',
});
});
});

View File

@@ -405,8 +405,8 @@ test.describe('pre-request features tests', async () => {
// verify
await page.getByRole('tab', { name: 'Timeline' }).click();
await expect(responsePane).toContainText('✓ happy tests'); // original proxy
await expect(responsePane).toContainText('✕ unhappy tests: AssertionError: expected 199 to deeply equal 200'); // updated proxy
await expect(responsePane).toContainText('✓ happy tests');
await expect(responsePane).toContainText('✕ unhappy tests: AssertionError: expected 199 to deeply equal 200');
});
test('environment and baseEnvironment can be persisted', async ({ page }) => {

View File

@@ -96,3 +96,37 @@ test.describe('test hidden window handling', async () => {
await expect(statusTag).toContainText('200 OK');
});
});
test('window should be restarted if it hangs', async ({ app, page }) => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
// load collection
const text = await loadFixture('pre-request-collection.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
await page.getByRole('button', { name: 'Create in project' }).click();
await page.getByRole('menuitemradio', { name: 'Import' }).click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
// update timeout
await page.getByTestId('settings-button').click();
await page.getByLabel('Request timeout (ms)').fill('100');
await page.getByRole('button', { name: '' }).click();
// send the request with infinite loop script
await page.getByText('Pre-request Scripts').click();
await page.getByLabel('Request Collection').getByTestId('infinite loop').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await page.getByText('Timeout: Hidden browser window is not responding').click();
// send the another script with normal script
await page.getByLabel('Request Collection').getByTestId('simple log').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// it should still work
const statusTag = page.locator('[data-testid="response-status-tag"]:visible');
await page.waitForSelector('[data-testid="response-status-tag"]:visible');
await expect(statusTag).toContainText('200 OK');
});

View File

@@ -200,7 +200,7 @@ describe('requestCreate()', () => {
parentId: 'wrk_123',
};
const r = await models.request.create(patch);
expect(Object.keys(r).length).toBe(23);
expect(Object.keys(r).length).toBe(24);
expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/);
expect(r.created).toBeGreaterThanOrEqual(now);
expect(r.modified).toBeGreaterThanOrEqual(now);

View File

@@ -576,6 +576,7 @@ export async function getRenderedRequestAndContext(
type: renderedRequest.type,
url: renderedRequest.url,
preRequestScript: renderedRequest.preRequestScript,
afterResponseScript: renderedRequest.afterResponseScript,
},
};
}

View File

@@ -34,7 +34,7 @@ export interface HiddenBrowserWindowToMainBridgeAPI {
curlRequest: (options: any) => Promise<any>;
readCurlResponse: (options: { bodyPath: string; bodyCompression: Compression }) => Promise<{ body: string; error: string }>;
setBusy: (busy: boolean) => void;
writeFile: (logPath: string, logContent: string) => Promise<void>;
appendFile: (logPath: string, logContent: string) => Promise<void>;
asyncTasksAllSettled: () => Promise<void>;
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,

View File

@@ -5,7 +5,7 @@ import * as _ from 'lodash';
import { invariant } from '../src/utils/invariant';
export interface HiddenBrowserWindowBridgeAPI {
runPreRequestScript: (options: { script: string; context: RequestContext }) => Promise<RequestContext>;
runScript: (options: { script: string; context: RequestContext }) => Promise<RequestContext>;
};
window.bridge.onmessage(async (data, callback) => {
@@ -18,7 +18,7 @@ window.bridge.onmessage(async (data, callback) => {
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<RequestContext> => {
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);

View File

@@ -22,6 +22,7 @@ describe('init()', () => {
parameters: [],
pathParameters: [],
preRequestScript: '',
afterResponseScript: '',
url: '',
settingStoreCookies: true,
settingSendCookies: true,
@@ -60,6 +61,7 @@ describe('create()', () => {
parameters: [],
pathParameters: [],
preRequestScript: '',
afterResponseScript: '',
url: '',
settingStoreCookies: true,
settingSendCookies: true,
@@ -394,6 +396,7 @@ describe('migrate()', () => {
parameters: [],
pathParameters: [],
preRequestScript: '',
afterResponseScript: '',
parentId: null,
body: {
mimeType: '',

View File

@@ -252,6 +252,7 @@ export interface BaseRequest {
method: string;
body: RequestBody;
preRequestScript: string;
afterResponseScript: string;
parameters: RequestParameter[];
pathParameters: RequestPathParameter[];
headers: RequestHeader[];
@@ -289,6 +290,7 @@ export function init(): BaseRequest {
method: METHOD_GET,
body: {},
preRequestScript: '',
afterResponseScript: '',
parameters: [],
headers: [],
authentication: {},

View File

@@ -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 {

View File

@@ -37,7 +37,7 @@ import {
smartEncodeUrl,
} from '../utils/url/querystring';
import { getAuthHeader, getAuthObjectOrNull, getAuthQueryParams, isAuthEnabled } from './authentication';
import { cancellableCurlRequest, cancellableRunPreRequestScript } from './cancellation';
import { cancellableCurlRequest, cancellableRunScript } from './cancellation';
import { filterClientCertificates } from './certificate';
import { addSetCookiesToToughCookieJar } from './set-cookie-util';
@@ -104,23 +104,93 @@ export const fetchRequestData = async (requestId: string) => {
const timelinePath = pathJoin(responsesDir, responseId + '.timeline');
return { request, environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId };
};
export const getPreRequestScriptOutput = async ({
request,
environment,
settings,
clientCertificates,
timelinePath,
responseId,
}: Awaited<ReturnType<typeof fetchRequestData>>, workspaceId: string) => {
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
export const tryToExecutePreRequestScript = async (
request: Request,
environment: Environment,
timelinePath: string,
responseId: string,
baseEnvironment: Environment,
clientCertificates: ClientCertificate[],
cookieJar: CookieJar,
) => {
if (!request.preRequestScript) {
return {
request,
environment: undefined,
baseEnvironment: undefined,
environment,
baseEnvironment,
clientCertificates,
settings,
};
}
const mutatedContext = await tryToExecutePreRequestScript({
request,
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;
}
await savePatchesMadeByScript(mutatedContext, environment, baseEnvironment);
return {
request: mutatedContext.request,
environment: mutatedContext.environment,
baseEnvironment: mutatedContext.baseEnvironment || baseEnvironment,
clientCertificates: mutatedContext.clientCertificates || clientCertificates,
settings: mutatedContext.settings || settings,
};
};
export async function savePatchesMadeByScript(
mutatedContext: Awaited<ReturnType<typeof tryToExecutePreRequestScript>>,
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 tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse) => {
const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response } = context;
invariant(script, 'script must be provided');
const settings = await models.settings.get();
try {
@@ -132,9 +202,13 @@ export const tryToExecutePreRequestScript = async (
// TODO: restart the hidden browser window
}, timeout + 1000);
});
const preRequestPromise = cancellableRunPreRequestScript({
script: request.preRequestScript,
// const isBaseEnvironmentSelected = environment._id === baseEnvironment._id;
// if (isBaseEnvironmentSelected) {
// // postman models base env as no env and does not persist, so we could handle that case better, but for now we throw
// throw new Error('Base environment cannot be selected for script execution. Please select an environment.');
// }
const executionPromise = cancellableRunScript({
script,
context: {
request,
timelinePath,
@@ -148,9 +222,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<string, any>;
baseEnvironment: Record<string, any>;
@@ -158,7 +233,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),
@@ -185,7 +260,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 = {
@@ -202,6 +280,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 tryToExecuteAfterResponseScript(context: RequestAndContextAndResponse) {
return tryToExecuteScript({ script: context.request.afterResponseScript, ...context });
}
export const tryToInterpolateRequest = async (
request: Request,
environment: string | Environment,
@@ -235,6 +337,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[],
@@ -242,7 +364,7 @@ export async function sendCurlAndWriteTimeline(
settings: Settings,
timelinePath: string,
responseId: string,
) {
): Promise<sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse> {
const requestId = renderedRequest._id;
const timelineStrings: string[] = [];
const authentication = renderedRequest.authentication as RequestAuthentication;
@@ -314,6 +436,7 @@ export async function sendCurlAndWriteTimeline(
...patch,
};
}
export const responseTransform = async (patch: ResponsePatch, environmentId: string | null, renderedRequest: RenderedRequest, context: Record<string, any>) => {
const response: ResponsePatch = {
...patch,

View File

@@ -197,6 +197,7 @@ describe('app.export.*', () => {
parameters: [],
pathParameters: [],
preRequestScript: '',
afterResponseScript: '',
parentId: 'wrk_1',
settingDisableRenderRequestBody: false,
settingEncodeUrl: true,

View File

@@ -71,7 +71,7 @@ const main: Window['main'] = {
},
},
hiddenBrowserWindow: {
runPreRequestScript: options => new Promise(async (resolve, reject) => {
runScript: options => new Promise(async (resolve, reject) => {
const isPortAlive = ports.get('hiddenWindowPort') !== undefined;
await ipcRenderer.invoke('open-channel-to-hidden-browser-window', isPortAlive);

View File

@@ -2,8 +2,8 @@ import { Snippet } from 'codemirror';
import { CookieObject, Environment, InsomniaObject, Request as ScriptRequest, RequestInfo, Url, Variables } from 'insomnia-sdk';
import React, { FC, useRef } from 'react';
import { translateHandlersInScript } from '../../../../src/utils/importers/importers/postman';
import { Settings } from '../../../models/settings';
import { translateHandlersInScript } from '../../../utils/importers/importers/postman';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
@@ -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/
@@ -80,7 +92,7 @@ const lintOptions = {
// TODO: introduce this functionality for other objects, such as Url, UrlMatchPattern and so on
// TODO: introduce function arguments
// TODO: provide snippets for environment keys if possible
function getPreRequestScriptSnippets(insomniaObject: InsomniaObject, path: string): Snippet[] {
function getRequestScriptSnippets(insomniaObject: InsomniaObject, path: string): Snippet[] {
let snippets: Snippet[] = [];
const refs = new Set();
@@ -117,17 +129,17 @@ function getPreRequestScriptSnippets(insomniaObject: InsomniaObject, path: strin
});
} else if (Array.isArray(value)) {
for (const item of value) {
snippets = snippets.concat(getPreRequestScriptSnippets(item, `${path}.${key}`));
snippets = snippets.concat(getRequestScriptSnippets(item, `${path}.${key}`));
}
} else {
snippets = snippets.concat(getPreRequestScriptSnippets(value, `${path}.${key}`));
snippets = snippets.concat(getRequestScriptSnippets(value, `${path}.${key}`));
}
}
return snippets;
}
export const PreRequestScriptEditor: FC<Props> = ({
export const RequestScriptEditor: FC<Props> = ({
className,
defaultValue,
onChange,
@@ -154,7 +166,7 @@ export const PreRequestScriptEditor: FC<Props> = ({
};
// TODO(george): Add more to this object to provide improved autocomplete
const preRequestScriptSnippets = getPreRequestScriptSnippets(
const requestScriptSnippets = getRequestScriptSnippets(
new InsomniaObject({
globals: new Environment('globals', {}),
iterationData: new Environment('iterationData', {}),
@@ -199,8 +211,8 @@ export const PreRequestScriptEditor: FC<Props> = ({
<div className='h-full flex flex-col'>
<div className="flex-1">
<CodeEditor
id={`script-editor-${uniquenessKey}`}
key={uniquenessKey}
id="pre-request-script-editor"
disableContextMenu={true}
showPrettifyButton={true}
uniquenessKey={uniquenessKey}
@@ -211,7 +223,7 @@ export const PreRequestScriptEditor: FC<Props> = ({
placeholder="..."
lintOptions={lintOptions}
ref={editorRef}
getAutocompleteSnippets={() => preRequestScriptSnippets}
getAutocompleteSnippets={() => requestScriptSnippets}
onPaste={translateHandlersInScript}
/>
</div>
@@ -380,6 +392,70 @@ export const PreRequestScriptEditor: FC<Props> = ({
/>
</DropdownItem>
</Dropdown>
<Dropdown
aria-label='Response Handling'
placement='top left'
triggerButton={
<DropdownButton>
<ItemContent
icon="code"
label='Response Handling'
/>
</DropdownButton>
}
>
<DropdownItem textValue='Get status code' arial-label={'Get status code'}>
<ItemContent
icon="circle-info"
label='Get status code'
onClick={() => addSnippet(getStatusCode)}
/>
</DropdownItem>
<DropdownItem textValue='Get status message' arial-label={'Get status message'}>
<ItemContent
icon="circle-info"
label='Get status message'
onClick={() => addSnippet(getStatusMsg)}
/>
</DropdownItem>
<DropdownItem textValue='Get response time' arial-label={'Get response time'}>
<ItemContent
icon="circle-info"
label='Get response time'
onClick={() => addSnippet(getRespTime)}
/>
</DropdownItem>
<DropdownItem textValue='Get body as JSON' arial-label={'Get body as JSON'}>
<ItemContent
icon="circle-info"
label='Get body as JSON'
onClick={() => addSnippet(getJsonBody)}
/>
</DropdownItem>
<DropdownItem textValue='Get body as text' arial-label={'Get body as text'}>
<ItemContent
icon="circle-info"
label='Get body as text'
onClick={() => addSnippet(getTextBody)}
/>
</DropdownItem>
<DropdownItem textValue='Find a header by name' arial-label={'Find a header by name'}>
<ItemContent
icon="circle-info"
label='Find a header by name'
onClick={() => addSnippet(findHeader)}
/>
</DropdownItem>
<DropdownItem textValue='Get cookies' arial-label={'Get cookies'}>
<ItemContent
icon="circle-info"
label='Get cookies'
onClick={() => addSnippet(getCookies)}
/>
</DropdownItem>
</Dropdown>
<Dropdown
aria-label='Misc'
placement='top left'

View File

@@ -19,9 +19,9 @@ import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { ContentTypeDropdown } from '../dropdowns/content-type-dropdown';
import { AuthWrapper } from '../editors/auth/auth-wrapper';
import { BodyEditor } from '../editors/body/body-editor';
import { PreRequestScriptEditor } from '../editors/pre-request-script-editor';
import { RequestHeadersEditor } from '../editors/request-headers-editor';
import { RequestParametersEditor } from '../editors/request-parameters-editor';
import { RequestScriptEditor } from '../editors/request-script-editor';
import { ErrorBoundary } from '../error-boundary';
import { Icon } from '../icon';
import { MarkdownPreview } from '../markdown-preview';
@@ -293,7 +293,7 @@ export const RequestPane: FC<Props> = ({
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<PreRequestScriptEditor
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.preRequestScript || ''}
onChange={preRequestScript => patchRequest(requestId, { preRequestScript })}
@@ -301,6 +301,33 @@ export const RequestPane: FC<Props> = ({
/>
</ErrorBoundary>
</TabItem>
<TabItem
key="after-response-script"
data-testid="after-response-script-tab"
title={
<div className='flex items-center gap-2'>
After-response Script{' '}
{activeRequest.afterResponseScript && (
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
</span>
)}
</div>
}
aria-label={'experimental'}
>
<ErrorBoundary
key={uniqueKey}
errorClassName="tall wide vertically-align font-error pad text-center"
>
<RequestScriptEditor
uniquenessKey={uniqueKey}
defaultValue={activeRequest.afterResponseScript || ''}
onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })}
settings={settings}
/>
</ErrorBoundary>
</TabItem>
<TabItem
key="docs"
title={

View File

@@ -26,7 +26,7 @@ import { Response } from '../../models/response';
import { isWebSocketRequest, isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
import { WebSocketResponse } from '../../models/websocket-response';
import { getAuthHeader } from '../../network/authentication';
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToExecutePreRequestScript, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../network/network';
import { fetchRequestData, getPreRequestScriptOutput, responseTransform, savePatchesMadeByScript, sendCurlAndWriteTimeline, tryToExecuteAfterResponseScript, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../network/network';
import { RenderErrorSubType } from '../../templating';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
@@ -363,69 +363,16 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
const { requestId, workspaceId } = params;
invariant(typeof requestId === 'string', 'Request ID is required');
invariant(workspaceId, 'Workspace ID is required');
const {
request: req,
environment,
settings,
clientCertificates,
caCert,
activeEnvironmentId,
timelinePath,
responseId,
} = await fetchRequestData(requestId);
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams;
try {
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams;
const mutatedContext = await tryToExecutePreRequestScript(
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?
const requestData = await fetchRequestData(requestId);
const mutatedContext = await getPreRequestScriptOutput(requestData, workspaceId);
if (mutatedContext === null) {
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,
}
);
}
}
const renderedResult = await tryToInterpolateRequest(
mutatedContext.request,
mutatedContext.environment || environment._id,
mutatedContext.environment,
RENDER_PURPOSE_SEND,
undefined,
mutatedContext.baseEnvironment,
@@ -448,30 +395,48 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
const response = await sendCurlAndWriteTimeline(
renderedRequest,
mutatedContext.clientCertificates || clientCertificates,
caCert,
mutatedContext.settings || settings,
timelinePath,
responseId
mutatedContext.clientCertificates,
requestData.caCert,
mutatedContext.settings,
requestData.timelinePath,
requestData.responseId
);
const requestMeta = await models.requestMeta.getByParentId(requestId);
invariant(requestMeta, 'RequestMeta not found');
const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, renderedResult.context);
const responsePatch = await responseTransform(response, requestData.activeEnvironmentId, renderedRequest, renderedResult.context);
const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath;
const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath;
if (requestData.request.afterResponseScript) {
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
const postMutatedContext = await tryToExecuteAfterResponseScript({
...requestData,
...mutatedContext,
baseEnvironment,
cookieJar,
response,
});
if (!postMutatedContext?.request) {
// exiy early if there was a problem with the pre-request script
// TODO: improve error message?
return null;
}
await savePatchesMadeByScript(postMutatedContext, requestData.environment, baseEnvironment);
}
if (!shouldWriteToFile) {
const response = await models.response.create(responsePatch, settings.maxHistoryResponses);
const response = await models.response.create(responsePatch, requestData.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
? contentDisposition.parse(header.value).parameters.filename
: `${req.name.replace(/\s/g, '-').toLowerCase()}.${responsePatch.contentType && mimeExtension(responsePatch.contentType) || 'unknown'}`;
return writeToDownloadPath(path.join(requestMeta.downloadPath, name), responsePatch, requestMeta, settings.maxHistoryResponses);
: `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${responsePatch.contentType && mimeExtension(responsePatch.contentType) || 'unknown'}`;
return writeToDownloadPath(path.join(requestMeta.downloadPath, name), responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
} else {
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
const { filePath } = await window.dialog.showSaveDialog({
@@ -485,7 +450,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
return null;
}
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
return writeToDownloadPath(filePath, responsePatch, requestMeta, settings.maxHistoryResponses);
return writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses);
}
} catch (e) {
console.log('Failed to send request', e);

View File

@@ -78,6 +78,7 @@ export interface ImportRequest<T extends {} = {}> extends Comment {
queryString?: QueryString[];
url?: string;
preRequestScript?: string;
afterResponseScript?: string;
metaSortKey?: number;
}

View File

@@ -3842,6 +3842,7 @@ exports[`Fixtures Import postman api-key-default-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"addTo": "header",
"disabled": false,
@@ -3883,6 +3884,7 @@ exports[`Fixtures Import postman api-key-header-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"addTo": "header",
"disabled": false,
@@ -3924,6 +3926,7 @@ exports[`Fixtures Import postman api-key-queryParams-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"addTo": "queryParams",
"disabled": false,
@@ -3965,6 +3968,7 @@ exports[`Fixtures Import postman aws-signature-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessKeyId": "aws-access-key",
"disabled": false,
@@ -4008,6 +4012,7 @@ exports[`Fixtures Import postman aws-signature-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessKeyId": "aws-access-key",
"disabled": false,
@@ -4051,6 +4056,7 @@ exports[`Fixtures Import postman basic-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "basic-password",
@@ -4091,6 +4097,7 @@ exports[`Fixtures Import postman basic-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "basic-password",
@@ -4131,6 +4138,7 @@ exports[`Fixtures Import postman bearer-token-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"prefix": "",
@@ -4171,6 +4179,7 @@ exports[`Fixtures Import postman bearer-token-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"prefix": "",
@@ -4211,6 +4220,7 @@ exports[`Fixtures Import postman complex-url-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "",
@@ -4252,6 +4262,7 @@ exports[`Fixtures Import postman complex-url-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "",
@@ -4302,6 +4313,7 @@ exports[`Fixtures Import postman complex-v2_0_fromHeaders-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"prefix": "",
@@ -4340,6 +4352,7 @@ exports[`Fixtures Import postman complex-v2_0_fromHeaders-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessKeyId": "<accessKeyId>",
"disabled": false,
@@ -4390,6 +4403,7 @@ exports[`Fixtures Import postman complex-v2_0_fromHeaders-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "abc123",
@@ -4428,6 +4442,7 @@ exports[`Fixtures Import postman complex-v2_0_fromHeaders-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "",
@@ -4473,6 +4488,7 @@ Child projects are provided as references; i.e. they only contain the <code>self
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "Callback%20URL",
"consumerKey": "Consumer%20Key",
@@ -4557,6 +4573,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "password",
@@ -4593,6 +4610,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "password",
@@ -4622,6 +4640,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "",
@@ -4640,6 +4659,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "Request with empty raw body",
@@ -4655,6 +4675,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "Request with unknown body",
@@ -4670,6 +4691,7 @@ exports[`Fixtures Import postman complex-v2_0-input.json 1`] = `
{
"_id": "__REQ_6__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "application/graphql",
@@ -4717,6 +4739,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "password",
@@ -4759,6 +4782,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "password",
@@ -4788,6 +4812,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "",
@@ -4806,6 +4831,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "Request with unknown body",
@@ -4821,6 +4847,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "Request with empty raw body",
@@ -4836,6 +4863,7 @@ exports[`Fixtures Import postman complex-v2_1-input.json 1`] = `
{
"_id": "__REQ_6__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "application/graphql",
@@ -4874,6 +4902,7 @@ exports[`Fixtures Import postman digest-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "guest",
@@ -4914,6 +4943,7 @@ exports[`Fixtures Import postman digest-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"password": "guest",
@@ -4954,6 +4984,7 @@ exports[`Fixtures Import postman faker-vars-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"disabled": false,
"prefix": "",
@@ -5009,6 +5040,7 @@ exports[`Fixtures Import postman minimal-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "",
@@ -5044,6 +5076,7 @@ exports[`Fixtures Import postman minimal-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {},
"description": "",
@@ -5079,6 +5112,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "consumer-key-value",
@@ -5109,6 +5143,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "consumer-key-value",
@@ -5139,6 +5174,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "plaintext-consumer-key-value",
@@ -5169,6 +5205,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "sha256-consumer-key-value",
@@ -5199,6 +5236,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "sha1-consumer-key-value",
@@ -5249,6 +5287,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "consumer-key-value",
@@ -5279,6 +5318,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "consumer-key-value",
@@ -5309,6 +5349,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "plaintext-consumer-key-value",
@@ -5339,6 +5380,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "sha256-consumer-key-value",
@@ -5369,6 +5411,7 @@ exports[`Fixtures Import postman oauth1_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"callback": "",
"consumerKey": "sha1-consumer-key-value",
@@ -5419,6 +5462,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "",
"authorizationUrl": "",
@@ -5442,6 +5486,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_0-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "",
"authorizationUrl": "",
@@ -5485,6 +5530,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "test",
"authorizationUrl": "test",
@@ -5517,6 +5563,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "test",
"authorizationUrl": "test",
@@ -5549,6 +5596,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "test",
"authorizationUrl": "test",
@@ -5581,6 +5629,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "test",
"authorizationUrl": "test",
@@ -5613,6 +5662,7 @@ exports[`Fixtures Import postman oauth2_0-auth-v2_1-input.json 1`] = `
{
"_id": "__REQ_5__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "test",
"authorizationUrl": "test",
@@ -5665,6 +5715,7 @@ exports[`Fixtures Import postman postman-export-oauth2-v2_1-input.json 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {
"accessTokenUrl": "exampleAccessTokenUrl",
"authorizationUrl": "exampleAuthorizeUrl",
@@ -5698,6 +5749,42 @@ exports[`Fixtures Import postman postman-export-oauth2-v2_1-input.json 1`] = `
}
`;
exports[`Fixtures Import postman scripts-import-v2_1-input.json 1`] = `
{
"__export_date": "",
"__export_format": 4,
"__export_source": "insomnia.importers:v0.1.0",
"_type": "export",
"resources": [
{
"_id": "__GRP_1__",
"_type": "request_group",
"description": "",
"environment": {},
"metaSortKey": -1622117984000,
"name": "New Collection",
"parentId": "__WORKSPACE_ID__",
},
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "console.log('this is test script');",
"authentication": {},
"body": {},
"description": "",
"headers": [],
"metaSortKey": -1622117983999,
"method": "GET",
"name": "New Request",
"parameters": [],
"parentId": "__GRP_1__",
"preRequestScript": "console.log('this is pre-request script');",
"url": "httpbin.org/get",
},
],
}
`;
exports[`Fixtures Import postman-env basic-input.json 1`] = `
{
"__export_date": "",
@@ -8064,6 +8151,7 @@ exports[`Fixtures Import wsdl addition-input.wsdl 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "text/xml",
@@ -8147,6 +8235,7 @@ exports[`Fixtures Import wsdl calculator-input.wsdl 1`] = `
{
"_id": "__REQ_1__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "text/xml",
@@ -8201,6 +8290,7 @@ exports[`Fixtures Import wsdl calculator-input.wsdl 1`] = `
{
"_id": "__REQ_2__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "text/xml",
@@ -8255,6 +8345,7 @@ exports[`Fixtures Import wsdl calculator-input.wsdl 1`] = `
{
"_id": "__REQ_3__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "text/xml",
@@ -8309,6 +8400,7 @@ exports[`Fixtures Import wsdl calculator-input.wsdl 1`] = `
{
"_id": "__REQ_4__",
"_type": "request",
"afterResponseScript": "",
"authentication": {},
"body": {
"mimeType": "text/xml",

View File

@@ -0,0 +1,50 @@
{
"info": {
"_postman_id": "c7c1ce35-b25b-4a79-85a7-8141e1f8cfba",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "30615465"
},
"item": [
{
"name": "New Request",
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
"console.log('this is pre-request script');"
],
"type": "text/javascript",
"packages": {}
}
},
{
"listen": "test",
"script": {
"exec": [
"console.log('this is test script');"
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "httpbin.org/get",
"host": [
"httpbin",
"org"
],
"path": [
"get"
]
}
},
"response": []
}
]
}

View File

@@ -170,6 +170,27 @@ export class ImportPostman {
return translateHandlersInScript(scriptContent);
};
importAfterResponseScript = (events: EventList | undefined): string => {
if (events == null) {
return '';
}
const afterResponseEvent = events.find(
event => event.listen === 'test'
);
const scriptOrRows = afterResponseEvent ? afterResponseEvent.script : '';
if (!scriptOrRows) {
return '';
}
const scriptContent = scriptOrRows.exec ?
(Array.isArray(scriptOrRows.exec) ? scriptOrRows.exec.join('\n') : scriptOrRows.exec) :
'';
return translateHandlersInScript(scriptContent);
};
importRequestItem = (
{ request, name = '', event }: Item,
parentId: string,
@@ -187,6 +208,8 @@ export class ImportPostman {
}
const preRequestScript = this.importPreRequestScript(event);
const afterResponseScript = this.importAfterResponseScript(event);
return {
parentId,
_id: `__REQ_${requestCount++}__`,
@@ -205,6 +228,7 @@ export class ImportPostman {
body: this.importBody(request.body, headers.find(({ key }) => key === 'Content-Type')?.value),
authentication,
preRequestScript,
afterResponseScript,
};
};