chore(pre-req): clean up pre-request script sdk objects - INS-3379 (#7172)

* feat: enable eval() in script

* feat: enable requiring node.js modules

* fix: request auth type is not correctly updated

* test: add tests for unphappy paths

* test: fixed a flaky test

* chore: make console.log outputs more friendly

* feat: enable requiring npm modules and built-in lodash

* fix: add prefix to 'fs' module requiring name as it is for internal usage

* fix: smoke test failure

* fix: use fs module directly to avoid exposing it

* fix: replace atob btoa with node.js modules

* chore: remove unused line

* fix: lint error

* fix: smoke test failed because fs no longer exposed

* fix: use builtin atob, btoa

* fix: disable lint error for builtin lodash

* chore: remove lodash as it's redundant

* chore: temporarily remove external dependencies

* fix: rebase and update smoke tests style

* chore: restore lock

* chore: remove comments

* chore: remove comments
This commit is contained in:
Hexxa authored and GitHub committed 2024-03-22 20:02:56 +08:00
1 parent 277599e7e6
commit a79b01e1e6
6 files changed
+325 -11

No files matched your search

@@ -603,3 +603,150 @@ resources:
mimeType: "application/json"
text: '{}'
_type: request
- _id: req_89dade2ee9ee42fbb22d588783a9df07
parentId: fld_01de564274824ecaad272330339ea6b2
modified: 1636707449231
created: 1636141014552
url: http://127.0.0.1:4010/echo
name: eval() works in script
description: ""
method: POST
parameters: []
headers:
- name: 'Content-Type'
value: 'application/json'
authentication: {}
metaSortKey: -1636141014553
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
preRequestScript: |-
const evalResult = eval('8+8');
insomnia.environment.set('evalResult', evalResult);
body:
mimeType: "application/json"
text: |-
{
"evalResult": {{ _.evalResult }}
}
_type: request
- _id: req_89dade2ee9ee42fbb22d588783a9df08
parentId: fld_01de564274824ecaad272330339ea6b2
modified: 1636707449231
created: 1636141014552
url: http://127.0.0.1:4010/echo
name: require the url module
description: ""
method: POST
parameters: []
headers:
- name: 'Content-Type'
value: 'application/json'
authentication: {}
metaSortKey: -1636141014553
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
preRequestScript: |-
const { URL } = require('url');
const url = new URL('https://user:pwd@insomnia.com:6666/p1?q1=a&q2=b#hashcontent');
insomnia.environment.set('hash', "#hashcontent");
insomnia.environment.set('host', "insomnia.com:6666");
insomnia.environment.set('hostname', "insomnia.com");
insomnia.environment.set('href', "https://user:pwd@insomnia.com:6666/p1?q1=a&q2=b#hashcontent");
insomnia.environment.set('origin', "https://insomnia.com:6666");
insomnia.environment.set('password', "pwd");
insomnia.environment.set('pathname', "/p1");
insomnia.environment.set('port', "6666");
insomnia.environment.set('protocol', "https:");
insomnia.environment.set('search', "?q1=a&q2=b");
insomnia.environment.set('username', "user");
insomnia.environment.set('seachParam', url.searchParams.toString());
body:
mimeType: "application/json"
text: |-
{
"hash": "{{ _.hash }}",
"host": "{{ _.host }}",
"hostname": "{{ _.hostname }}",
"href": "{{ _.href }}",
"origin": "{{ _.origin }}",
"password": "{{ _.password }}",
"pathname": "{{ _.pathname }}",
"port": "{{ _.port }}",
"protocol": "{{ _.protocol }}",
"search": "{{ _.search }}",
"username": "{{ _.username }}",
"seachParam": "{{ _.seachParam }}"
}
_type: request
- _id: req_89dade2ee9ee42fbb22d588783a9df09
parentId: fld_01de564274824ecaad272330339ea6b2
modified: 1636707449231
created: 1636141014552
url: http://127.0.0.1:4010/echo
name: require node.js modules
description: ""
method: POST
parameters: []
headers:
- name: 'Content-Type'
value: 'application/json'
authentication: {}
metaSortKey: -1636141014553
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
preRequestScript: |-
const path = require('path');
const assert = require('assert');
const buffer = require('buffer');
const util = require('util');
const url = require('url');
const punycode = require('punycode');
const querystring = require('querystring');
const stringDecoder = require('string_decoder');
const stream = require('stream');
const timers = require('timers');
const events = require('events');
// set them
insomnia.environment.set('path', path != null);
insomnia.environment.set('assert', assert != null);
insomnia.environment.set('buffer', buffer != null);
insomnia.environment.set('util', util != null);
insomnia.environment.set('url', url != null);
insomnia.environment.set('punycode', punycode != null);
insomnia.environment.set('querystring', querystring != null);
insomnia.environment.set('stringDecoder', stringDecoder != null);
insomnia.environment.set('stream', stream != null);
insomnia.environment.set('timers', timers != null);
insomnia.environment.set('events', events != null);
body:
mimeType: "application/json"
text: |-
{
"path": {{ _.path }},
"assert": {{ _.assert }},
"buffer": {{ _.buffer }},
"util": {{ _.util }},
"url": {{ _.url }},
"punycode": {{ _.punycode }},
"querystring": {{ _.querystring }},
"stringDecoder": {{ _.stringDecoder }},
"stream": {{ _.stream }},
"timers": {{ _.timers }},
"events": {{ _.events }}
}
_type: request
@@ -84,6 +84,48 @@ test.describe('pre-request features tests', async () => {
expect(bodyJson.headers['authorization']).toEqual(`Basic ${expectedEncCred}`);
},
},
{
name: 'eval() works in script',
expectedBody: {
evalResult: 16,
},
},
{
name: 'require the url module',
customVerify: (bodyJson: any) => {
const reqBodyJsons = JSON.parse(bodyJson.data);
expect(reqBodyJsons).toEqual({
hash: '#hashcontent',
host: 'insomnia.com:6666',
hostname: 'insomnia.com',
href: 'https://user:pwd@insomnia.com:6666/p1?q1=a&q2=b#hashcontent',
origin: 'https://insomnia.com:6666',
password: 'pwd',
pathname: '/p1',
port: '6666',
protocol: 'https:',
search: '?q1=a&q2=b',
username: 'user',
seachParam: 'q1=a&q2=b',
});
},
},
{
name: 'require node.js modules',
expectedBody: {
path: true,
assert: true,
buffer: true,
util: true,
url: true,
punycode: true,
querystring: true,
stringDecoder: true,
stream: true,
timers: true,
events: true,
},
},
];
for (let i = 0; i < testCases.length; i++) {
@@ -108,7 +150,6 @@ test.describe('pre-request features tests', async () => {
const bodyJson = JSON.parse(rows.join(' '));
if (tc.expectedBody) {
// await page.waitForTimeout(600000);
expect(JSON.parse(bodyJson.data)).toEqual(tc.expectedBody);
}
if (tc.customVerify) {
@@ -304,3 +345,91 @@ test.describe('pre-request features tests', async () => {
await expect(responsePane).toContainText('fixtures/certificates/fake.pfx'); // original proxy
});
});
test.describe('unhappy paths', async () => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
test.beforeEach(async ({ app, page }) => {
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();
await page.getByLabel('Pre-request Scripts').click();
});
const testCases = [
{
name: 'invalid result is returned',
preReqScript: `
return;
`,
context: {
insomnia: {},
},
expectedResult: {
message: 'insomnia object is invalid or script returns earlier than expected.',
},
},
{
name: 'custom error is returned',
preReqScript: `
throw Error('my custom error');
`,
context: {
insomnia: {},
},
expectedResult: {
message: 'my custom error',
},
},
{
name: 'syntax error',
preReqScript: `
insomnia.INVALID_FIELD.set('', '')
`,
context: {
insomnia: {},
},
expectedResult: {
message: "Cannot read properties of undefined (reading 'set')",
},
},
];
for (let i = 0; i < testCases.length; i++) {
const tc = testCases[i];
test(tc.name, async ({ page }) => {
const responsePane = page.getByTestId('response-pane');
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
// set request body
await page.getByRole('tab', { name: 'Body' }).click();
await page.getByRole('button', { name: 'Body' }).click();
await page.getByRole('menuitem', { name: 'JSON' }).click();
// enter script
const preRequestScriptTab = page.getByRole('tab', { name: 'Pre-request Script' });
await preRequestScriptTab.click();
const preRequestScriptEditor = page.getByTestId('CodeEditor').getByRole('textbox');
await preRequestScriptEditor.fill(tc.preReqScript);
// TODO: wait for body and pre-request script are persisted to the disk
// should improve this part, we should avoid sync this state through db as it introduces race condition
await page.waitForTimeout(500);
// send
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify
await page.waitForSelector('[data-testid="response-status-tag"]:visible');
await expect(responsePane).toContainText(tc.expectedResult.message);
});
}
});
@@ -40,11 +40,6 @@ test('handle hidden browser window getting closed', async ({ app, page }) => {
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
await page.getByText('Pre-request Scripts').click();
await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await page.getByTestId('settings-button').click();
await page.getByLabel('Request timeout (ms)').fill('1');
await page.getByRole('button', { name: '' }).click();
+25 -1
View File
@@ -27,8 +27,32 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {
},
requireInterceptor: (moduleName: string) => {
if (['uuid', 'fs'].includes(moduleName)) {
if (
[
// node.js modules
'path',
'assert',
'buffer',
'util',
'url',
'punycode',
'querystring',
'string_decoder',
'stream',
'timers',
'events',
// follows should be npm modules
// but they are moved to here to avoid introducing additional dependencies
].includes(moduleName)
) {
return require(moduleName);
} else if (
[
'atob',
'btoa',
].includes(moduleName)
) {
return moduleName === 'atob' ? atob : btoa;
} else if (moduleName === 'insomnia-collection' || moduleName === 'postman-collection') {
return CollectionModule;
}
+22 -4
View File
@@ -1,4 +1,9 @@
import { initInsomniaObject } from './sdk/objects/insomnia';
import * as fs from 'node:fs';
import * as _ from 'lodash';
import { invariant } from '../src/utils/invariant';
import { initInsomniaObject, InsomniaObject } from './sdk/objects/insomnia';
import { RequestContext } from './sdk/objects/interfaces';
import { mergeClientCertificates, mergeRequests, mergeSettings } from './sdk/objects/request';
@@ -37,18 +42,26 @@ const runPreRequestScript = async (
const consoleInterceptor = {
log: (...args: any[]) => log.push(
JSON.stringify({
value: args.map(a => JSON.stringify(a)).join('\n'),
value: args.map(a => JSON.stringify(a, null, 2)).join('\n'),
name: 'Text',
timestamp: Date.now(),
}) + '\n',
),
};
const evalInterceptor = (script: string) => {
invariant(script && typeof script === 'string', 'eval is called with invalid or empty value');
const result = eval(script);
return result;
};
const AsyncFunction = (async () => { }).constructor;
const executeScript = AsyncFunction(
'insomnia',
'require',
'console',
'eval',
'_',
`
const $ = insomnia, pm = insomnia;
${script};
@@ -58,14 +71,19 @@ const runPreRequestScript = async (
const mutatedInsomniaObject = await executeScript(
executionContext,
window.bridge.requireInterceptor,
consoleInterceptor
consoleInterceptor,
evalInterceptor,
_,
);
if (mutatedInsomniaObject == null || !(mutatedInsomniaObject instanceof InsomniaObject)) {
throw Error('insomnia object is invalid or script returns earlier than expected.');
}
const mutatedContextObject = mutatedInsomniaObject.toObject();
const updatedRequest = mergeRequests(context.request, mutatedContextObject.request);
const updatedSettings = mergeSettings(context.settings, mutatedContextObject.request);
const updatedCertificates = mergeClientCertificates(context.clientCertificates, mutatedContextObject.request);
await window.bridge.requireInterceptor('fs').promises.writeFile(context.timelinePath, log.join('\n'));
await fs.promises.writeFile(context.timelinePath, log.join('\n'));
console.log('mutatedInsomniaObject', mutatedContextObject);
console.log('context', context);
@@ -66,6 +66,7 @@ const lintOptions = {
pm: true,
require: true,
console: true,
_: true,
},
asi: true,
// Don't require semicolons