feat: extend platform API support for experimental scripting sandbox (#5097)

This commit is contained in:
James George 2025-05-26 20:39:51 +05:30 committed by GitHub
parent 935bc10c0b
commit aab2924139
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 560 additions and 541 deletions

View file

@ -180,6 +180,44 @@ describe("hopp test [options] <file_path_or_id>", { timeout: 100000 }, () => {
expect(error).toBeNull();
});
});
test("Successfully display console logs and recognizes platform APIs in the experimental scripting sandbox", async () => {
const args = `test ${getTestJsonFilePath(
"test-scripting-sandbox-modes-coll.json",
"collection"
)}`;
const { error, stdout } = await runCLI(args);
expect(error).toBeNull();
const expectedStaticParts = [
"https://example.com/path?foo=bar&baz=qux",
"'0': 72",
"'12': 33",
"Decoded: Hello, world!",
"Hello after 1s",
];
// Assert that each stable part appears in the output
expectedStaticParts.forEach((part) => {
expect(stdout).toContain(part);
});
const every500msCount = (stdout.match(/Every 500ms/g) || []).length;
expect(every500msCount).toBe(3);
});
test("Fails to display console logs and recognize platform APIs in the legacy scripting sandbox", async () => {
const args = `test ${getTestJsonFilePath(
"test-scripting-sandbox-modes-coll.json",
"collection"
)} --legacy-sandbox`;
const { error, stdout } = await runCLI(args);
expect(error).toBeTruthy();
expect(stdout).not.toContain("https://example.com/path?foo=bar&baz=qux");
expect(stdout).not.toContain("Encoded");
});
});
test("Ensures tests run in sequence order based on request path", async () => {

View file

@ -0,0 +1,34 @@
{
"v": 7,
"id": "cmb4vtsqh00nxwvqryk6jmnaz",
"name": "test-scripting-sandbox-modes",
"folders": [],
"requests": [
{
"v": "12",
"name": "sample-req",
"method": "GET",
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "const url = new URL('https://example.com/path?foo=bar');\nurl.searchParams.set('baz', 'qux');\nurl.toString(); // 'https://example.com/path?foo=bar&baz=qux'\n\nconsole.debug(url)\n\nconst encoder = new TextEncoder();\n\nconst text = \"Hello, world!\";\n\nconst encoded = encoder.encode(text);\nconsole.log(\"Encoded:\", encoded);\n\nconst decoder = new TextDecoder();\n\nconst decoded = decoder.decode(encoded);\nconsole.log(\"Decoded:\", decoded);\n\nsetTimeout(() => console.log(\"Hello after 1s\"), 1000);\n \nconst intervalId = setInterval(() => console.log(\"Every 500ms\"), 500);\n\nsetTimeout(() => clearInterval(intervalId), 2000);",
"testScript": "console.log(JSON.stringify(pw, null, 2))\n\n\npw.test(\"Sample assertion\", ()=> {\n pw.expect(pw.response.status).toBeLevel2xx()\n \tconsole.log(\"Status code received is \", pw.response.status)\n});\n\n",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"contentType": null,
"body": null
},
"requestVariables": [],
"responses": {}
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [],
"_ref_id": "coll_mb4vvacd_e015964c-b5c8-4b90-a564-4a8b62bc1631"
}

View file

@ -53,7 +53,7 @@
"dependencies": {
"@hoppscotch/data": "workspace:^",
"@types/lodash-es": "4.17.12",
"faraday-cage": "0.0.15",
"faraday-cage": "0.0.16",
"fp-ts": "2.16.9",
"lodash": "4.17.21",
"lodash-es": "4.17.21"

View file

@ -9,52 +9,37 @@
resolve: (key) => inputs.envResolve(key),
},
expect: (expectVal) => {
return {
const isDateInstance = expectVal instanceof Date
const expectation = {
toBe: (expectedVal) => inputs.expectToBe(expectVal, expectedVal),
toBeLevel2xx: (expectedVal) =>
inputs.expectToBeLevel2xx(expectVal, expectedVal),
toBeLevel3xx: (expectedVal) =>
inputs.expectToBeLevel3xx(expectVal, expectedVal),
toBeLevel4xx: (expectedVal) =>
inputs.expectToBeLevel4xx(expectVal, expectedVal),
toBeLevel5xx: (expectedVal) =>
inputs.expectToBeLevel5xx(expectVal, expectedVal),
toBeType: (expectedVal) => {
const isExpectValDateInstance = expectVal instanceof Date
return inputs.expectToBeType(
expectVal,
expectedVal,
isExpectValDateInstance
)
},
toHaveLength: (expectedVal) =>
inputs.expectToHaveLength(expectVal, expectedVal),
toInclude: (expectedVal) =>
inputs.expectToInclude(expectVal, expectedVal),
not: {
toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
toBeLevel2xx: (expectedVal) =>
inputs.expectNotToBeLevel2xx(expectVal, expectedVal),
toBeLevel3xx: (expectedVal) =>
inputs.expectNotToBeLevel3xx(expectVal, expectedVal),
toBeLevel4xx: (expectedVal) =>
inputs.expectNotToBeLevel4xx(expectVal, expectedVal),
toBeLevel5xx: (expectedVal) =>
inputs.expectNotToBeLevel5xx(expectVal, expectedVal),
toBeType: (expectedVal) => {
const isExpectValDateInstance = expectVal instanceof Date
return inputs.expectNotToBeType(
expectVal,
expectedVal,
isExpectValDateInstance
)
},
toHaveLength: (expectedVal) =>
inputs.expectNotToHaveLength(expectVal, expectedVal),
toInclude: (expectedVal) =>
inputs.expectNotToInclude(expectVal, expectedVal),
},
toBeLevel2xx: () => inputs.expectToBeLevel2xx(expectVal),
toBeLevel3xx: () => inputs.expectToBeLevel3xx(expectVal),
toBeLevel4xx: () => inputs.expectToBeLevel4xx(expectVal),
toBeLevel5xx: () => inputs.expectToBeLevel5xx(expectVal),
toBeType: (expectedType) =>
inputs.expectToBeType(expectVal, expectedType, isDateInstance),
toHaveLength: (expectedLength) =>
inputs.expectToHaveLength(expectVal, expectedLength),
toInclude: (needle) => inputs.expectToInclude(expectVal, needle),
}
Object.defineProperty(expectation, "not", {
get: () => ({
toBe: (expectedVal) => inputs.expectNotToBe(expectVal, expectedVal),
toBeLevel2xx: () => inputs.expectNotToBeLevel2xx(expectVal),
toBeLevel3xx: () => inputs.expectNotToBeLevel3xx(expectVal),
toBeLevel4xx: () => inputs.expectNotToBeLevel4xx(expectVal),
toBeLevel5xx: () => inputs.expectNotToBeLevel5xx(expectVal),
toBeType: (expectedType) =>
inputs.expectNotToBeType(expectVal, expectedType, isDateInstance),
toHaveLength: (expectedLength) =>
inputs.expectNotToHaveLength(expectVal, expectedLength),
toInclude: (needle) => inputs.expectNotToInclude(expectVal, needle),
}),
})
return expectation
},
test: (descriptor, testFn) => {
inputs.preTest(descriptor)

View file

@ -0,0 +1,68 @@
import {
blobPolyfill,
ConsoleEntry,
console as ConsoleModule,
crypto,
encoding,
esmModuleLoader,
fetch,
urlPolyfill,
timers,
} from "faraday-cage/modules"
type DefaultModulesConfig = {
handleConsoleEntry: (consoleEntries: ConsoleEntry) => void
}
export const defaultModules = (config?: DefaultModulesConfig) => {
return [
urlPolyfill,
blobPolyfill,
ConsoleModule({
onLog(level, ...args) {
console[level](...args)
config?.handleConsoleEntry({
type: level,
args,
timestamp: Date.now(),
})
},
onCount(...args) {
console.count(args[0])
},
onTime(...args) {
console.timeEnd(args[0])
},
onTimeLog(...args) {
console.timeLog(...args)
},
onGroup(...args) {
console.group(...args)
},
onGroupEnd(...args) {
console.groupEnd(...args)
},
onClear(...args) {
console.clear(...args)
},
onAssert(...args) {
console.assert(...args)
},
onDir(...args) {
console.dir(...args)
},
onTable(...args) {
console.table(...args)
},
}),
crypto({
cryptoImpl: globalThis.crypto,
}),
esmModuleLoader,
fetch(),
encoding(),
timers(),
]
}

View file

@ -0,0 +1,2 @@
export { defaultModules } from "./default"
export { pwPostRequestModule, pwPreRequestModule } from "./pw"

View file

@ -261,8 +261,8 @@ const createPwModule = (
})
}
export const pwPostRequestModule = (config: PwPostRequestModuleConfig) =>
createPwModule("post", postRequestBootstrapCode, config)
export const pwPreRequestModule = (config: PwPreRequestModuleConfig) =>
createPwModule("pre", preRequestBootstrapCode, config)
export const pwPostRequestModule = (config: PwPostRequestModuleConfig) =>
createPwModule("post", postRequestBootstrapCode, config)

View file

@ -1,165 +0,0 @@
import { FaradayCage } from "faraday-cage"
import {
blobPolyfill,
console as ConsoleModule,
crypto,
esmModuleLoader,
fetch,
} from "faraday-cage/modules"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import type ivmT from "isolated-vm"
import { cloneDeep } from "lodash"
import { pwPreRequestModule } from "~/cage-modules/pw"
import { createRequire } from "module"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/shared-utils"
import { getSerializedAPIMethods } from "./utils"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
): TE.TaskEither<string, TestResult["envs"]> => {
if (!experimentalScriptingSandbox) {
return pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () => {
const jail = context.global
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
const serializedAPIMethods = getSerializedAPIMethods(pw)
jail.setSync("serializedAPIMethods", serializedAPIMethods, {
copy: true,
})
jail.setSync("atob", atob)
jail.setSync("btoa", btoa)
// Methods in the isolate context can't be invoked straightaway
const finalScript = `
const pw = new Proxy(serializedAPIMethods, {
get: (pwObjTarget, pwObjProp) => {
const topLevelEntry = pwObjTarget[pwObjProp]
// "pw.env" set of API methods
if (topLevelEntry && typeof topLevelEntry === "object") {
return new Proxy(topLevelEntry, {
get: (subTarget, subProp) => {
const subLevelProperty = subTarget[subProp]
if (subLevelProperty && subLevelProperty.typeof === "function") {
return (...args) => subLevelProperty.applySync(null, args)
}
},
})
}
}
})
${preRequestScript}
`
// Create a script and compile it
const script = await isolate.compileScript(finalScript)
// Run the pre-request script in the provided context
await script.run(context)
return updatedEnvs
},
(reason) => reason
),
TE.fold(
(error) => TE.left(`Script execution failed: ${error}`),
(result) =>
pipe(
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeError) => `Isolate disposal failed: ${disposeError}`
)
)
)
)
)
)
}
return pipe(
TE.tryCatch(
async (): Promise<TestResult["envs"]> => {
let finalEnvs = envs
const cage = await FaradayCage.create()
const result = await cage.runCode(preRequestScript, [
pwPreRequestModule({
envs: cloneDeep(envs),
handleSandboxResults: ({ envs }) => {
finalEnvs = envs
},
}),
blobPolyfill,
ConsoleModule({
onLog(...args) {
console[args[0]](...args)
},
onCount(...args) {
console.count(args[0])
},
onTime(...args) {
console.timeEnd(args[0])
},
onTimeLog(...args) {
console.timeLog(...args)
},
onGroup(...args) {
console.group(...args)
},
onGroupEnd(...args) {
console.groupEnd(...args)
},
onClear(...args) {
console.clear(...args)
},
onAssert(...args) {
console.assert(...args)
},
onDir(...args) {
console.dir(...args)
},
onTable(...args) {
console.table(...args)
},
}),
crypto(),
esmModuleLoader,
fetch(),
])
if (result.type === "error") {
throw result.err
}
return finalEnvs
},
(error) => {
if (error !== null && typeof error === "object" && "message" in error) {
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
return `Script execution failed: ${reason}`
}
return `Script execution failed: ${String(error)}`
}
)
)
}

View file

@ -0,0 +1,47 @@
import { FaradayCage } from "faraday-cage"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import { cloneDeep } from "lodash"
import { defaultModules, pwPreRequestModule } from "~/cage-modules"
import { TestResult } from "~/types"
export const runPreRequestScriptWithFaradayCage = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> => {
return pipe(
TE.tryCatch(
async (): Promise<TestResult["envs"]> => {
let finalEnvs = envs
const cage = await FaradayCage.create()
const result = await cage.runCode(preRequestScript, [
...defaultModules(),
pwPreRequestModule({
envs: cloneDeep(envs),
handleSandboxResults: ({ envs }) => {
finalEnvs = envs
},
}),
])
if (result.type === "error") {
throw result.err
}
return finalEnvs
},
(error) => {
if (error !== null && typeof error === "object" && "message" in error) {
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
return `Script execution failed: ${reason}`
}
return `Script execution failed: ${String(error)}`
}
)
)
}

View file

@ -0,0 +1,14 @@
import * as TE from "fp-ts/lib/TaskEither"
import { TestResult } from "~/types"
import { runPreRequestScriptWithFaradayCage } from "./experimental"
import { runPreRequestScriptWithIsolatedVm } from "./legacy"
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
): TE.TaskEither<string, TestResult["envs"]> =>
experimentalScriptingSandbox
? runPreRequestScriptWithFaradayCage(preRequestScript, envs)
: runPreRequestScriptWithIsolatedVm(preRequestScript, envs)

View file

@ -0,0 +1,82 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import type ivmT from "isolated-vm"
import { createRequire } from "module"
import { getPreRequestScriptMethods } from "~/shared-utils"
import { TestResult } from "~/types"
import { getSerializedAPIMethods } from "../utils"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
export const runPreRequestScriptWithIsolatedVm = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> => {
return pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () => {
const jail = context.global
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
const serializedAPIMethods = getSerializedAPIMethods(pw)
jail.setSync("serializedAPIMethods", serializedAPIMethods, {
copy: true,
})
jail.setSync("atob", atob)
jail.setSync("btoa", btoa)
// Methods in the isolate context can't be invoked straightaway
const finalScript = `
const pw = new Proxy(serializedAPIMethods, {
get: (pwObjTarget, pwObjProp) => {
const topLevelEntry = pwObjTarget[pwObjProp]
// "pw.env" set of API methods
if (topLevelEntry && typeof topLevelEntry === "object") {
return new Proxy(topLevelEntry, {
get: (subTarget, subProp) => {
const subLevelProperty = subTarget[subProp]
if (subLevelProperty && subLevelProperty.typeof === "function") {
return (...args) => subLevelProperty.applySync(null, args)
}
},
})
}
}
})
${preRequestScript}
`
// Create a script and compile it
const script = await isolate.compileScript(finalScript)
// Run the pre-request script in the provided context
await script.run(context)
return updatedEnvs
},
(reason) => reason
),
TE.fold(
(error) => TE.left(`Script execution failed: ${error}`),
(result) =>
pipe(
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeError) => `Isolate disposal failed: ${disposeError}`
)
)
)
)
)
)
}

View file

@ -0,0 +1,59 @@
import { FaradayCage } from "faraday-cage"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash"
import { defaultModules, pwPostRequestModule } from "~/cage-modules"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
export const runTestScriptWithFaradayCage = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
): TE.TaskEither<string, TestResult> => {
return pipe(
TE.tryCatch(
async (): Promise<TestResult> => {
const testRunStack: TestDescriptor[] = [
{ descriptor: "root", expectResults: [], children: [] },
]
let finalEnvs = envs
let finalTestResults = testRunStack
const cage = await FaradayCage.create()
const result = await cage.runCode(testScript, [
...defaultModules(),
pwPostRequestModule({
envs: cloneDeep(envs),
testRunStack: cloneDeep(testRunStack),
response,
handleSandboxResults: ({ envs, testRunStack }) => {
finalEnvs = envs
finalTestResults = testRunStack
},
}),
])
if (result.type === "error") {
throw result.err
}
return {
tests: finalTestResults,
envs: finalEnvs,
}
},
(error) => {
if (error !== null && typeof error === "object" && "message" in error) {
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
return `Script execution failed: ${reason}`
}
return `Script execution failed: ${String(error)}`
}
)
)
}

View file

@ -0,0 +1,24 @@
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { preventCyclicObjects } from "~/shared-utils"
import { TestResponse, TestResult } from "~/types"
import { runTestScriptWithFaradayCage } from "./experimental"
import { runTestScriptWithIsolatedVm } from "./legacy"
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
): TE.TaskEither<string, TestResult> => {
const responseObjHandle = preventCyclicObjects<TestResponse>(response)
if (E.isLeft(responseObjHandle)) {
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
}
return experimentalScriptingSandbox
? runTestScriptWithFaradayCage(testScript, envs, responseObjHandle.right)
: runTestScriptWithIsolatedVm(testScript, envs, responseObjHandle.right)
}

View file

@ -1,159 +1,19 @@
import { FaradayCage } from "faraday-cage"
import {
blobPolyfill,
console as ConsoleModule,
crypto,
esmModuleLoader,
fetch,
} from "faraday-cage/modules"
import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import type ivmT from "isolated-vm"
import { cloneDeep } from "lodash"
import { createRequire } from "module"
import { pwPostRequestModule } from "~/cage-modules/pw"
import {
getTestRunnerScriptMethods,
preventCyclicObjects,
} from "~/shared-utils"
import { TestDescriptor, TestResponse, TestResult } from "~/types"
import { getSerializedAPIMethods } from "./utils"
import { TestResponse, TestResult } from "~/types"
import { getSerializedAPIMethods } from "../utils"
const nodeRequire = createRequire(import.meta.url)
const ivm = nodeRequire("isolated-vm")
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
): TE.TaskEither<string, TestResult> => {
const responseObjHandle = preventCyclicObjects(response)
if (E.isLeft(responseObjHandle)) {
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
}
if (!experimentalScriptingSandbox) {
return pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () =>
executeScriptInContext(
testScript,
envs,
response,
isolate,
context
),
(reason) => `Script execution failed: ${reason}`
),
TE.chain((result) =>
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeReason) => `Isolate disposal failed: ${disposeReason}`
)
)
)
)
)
}
return pipe(
TE.tryCatch(
async (): Promise<TestResult> => {
const testRunStack: TestDescriptor[] = [
{ descriptor: "root", expectResults: [], children: [] },
]
let finalEnvs = envs
let finalTestResults = testRunStack
const cage = await FaradayCage.create()
const result = await cage.runCode(testScript, [
pwPostRequestModule({
envs: cloneDeep(envs),
testRunStack: cloneDeep(testRunStack),
response,
handleSandboxResults: ({ envs, testRunStack }) => {
finalEnvs = envs
finalTestResults = testRunStack
},
}),
blobPolyfill,
ConsoleModule({
onLog(...args) {
console[args[0]](...args)
},
onCount(...args) {
console.count(args[0])
},
onTime(...args) {
console.timeEnd(args[0])
},
onTimeLog(...args) {
console.timeLog(...args)
},
onGroup(...args) {
console.group(...args)
},
onGroupEnd(...args) {
console.groupEnd(...args)
},
onClear(...args) {
console.clear(...args)
},
onAssert(...args) {
console.assert(...args)
},
onDir(...args) {
console.dir(...args)
},
onTable(...args) {
console.table(...args)
},
}),
crypto(),
esmModuleLoader,
fetch(),
])
if (result.type === "error") {
throw result.err
}
return {
tests: finalTestResults,
envs: finalEnvs,
}
},
(error) => {
if (error !== null && typeof error === "object" && "message" in error) {
const reason = `${"name" in error ? error.name : ""}: ${error.message}`
return `Script execution failed: ${reason}`
}
return `Script execution failed: ${String(error)}`
}
)
)
}
const executeScriptInContext = (
testScript: string,
envs: TestResult["envs"],
@ -163,7 +23,7 @@ const executeScriptInContext = (
): Promise<TestResult> => {
return new Promise((resolve, reject) => {
// Parse response object
const responseObjHandle = preventCyclicObjects(response)
const responseObjHandle = preventCyclicObjects<TestResponse>(response)
if (E.isLeft(responseObjHandle)) {
return reject(`Response parsing failed: ${responseObjHandle.left}`)
}
@ -315,3 +175,44 @@ const executeScriptInContext = (
})
})
}
export const runTestScriptWithIsolatedVm = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
): TE.TaskEither<string, TestResult> => {
return pipe(
TE.tryCatch(
async () => {
const isolate: ivmT.Isolate = new ivm.Isolate()
const context = await isolate.createContext()
return { isolate, context }
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain(({ isolate, context }) =>
pipe(
TE.tryCatch(
async () =>
executeScriptInContext(
testScript,
envs,
response,
isolate,
context
),
(reason) => `Script execution failed: ${reason}`
),
TE.chain((result) =>
TE.tryCatch(
async () => {
await isolate.dispose()
return result
},
(disposeReason) => `Isolate disposal failed: ${disposeReason}`
)
)
)
)
)
}

View file

@ -213,9 +213,9 @@ const getResolvedExpectValue = (expectVal: any) => {
}
}
export function preventCyclicObjects(
obj: Record<string, any>
): E.Left<string> | E.Right<Record<string, any>> {
export function preventCyclicObjects<T extends object = Record<string, any>>(
obj: T
): E.Left<string> | E.Right<T> {
let jsonString
try {

View file

@ -1,97 +1,52 @@
import { FaradayCage } from "faraday-cage"
import { ConsoleEntry } from "faraday-cage/modules"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash"
import { SandboxPreRequestResult, TestResult } from "~/types"
import { FaradayCage } from "faraday-cage"
import {
blobPolyfill,
ConsoleEntry,
console as ConsoleModule,
crypto,
esmModuleLoader,
fetch,
} from "faraday-cage/modules"
import { cloneDeep } from "lodash"
import * as TE from "fp-ts/lib/TaskEither"
import { pwPreRequestModule } from "~/cage-modules/pw"
import { defaultModules, pwPreRequestModule } from "~/cage-modules"
import Worker from "./worker?worker&inline"
export const runPreRequestScript = async (
const runPreRequestScriptWithWebWorker = (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
envs: TestResult["envs"]
): Promise<E.Either<string, SandboxPreRequestResult>> => {
return new Promise((resolve) => {
const worker = new Worker()
// Listen for the results from the web worker
worker.addEventListener("message", (event: MessageEvent) => {
worker.terminate()
return resolve(event.data.results)
})
// Send the script to the web worker
worker.postMessage({
preRequestScript,
envs,
})
})
}
const runPreRequestScriptWithFaradayCage = async (
preRequestScript: string,
envs: TestResult["envs"]
): Promise<E.Either<string, SandboxPreRequestResult>> => {
const consoleEntries: ConsoleEntry[] = []
let finalEnvs = envs
if (!experimentalScriptingSandbox) {
return new Promise((resolve) => {
const worker = new Worker()
// Listen for the results from the web worker
worker.addEventListener("message", (event: MessageEvent) =>
resolve(event.data.results)
)
// Send the script to the web worker
worker.postMessage({
preRequestScript,
envs,
})
})
}
const cage = await FaradayCage.create()
const result = await cage.runCode(preRequestScript, [
...defaultModules({
handleConsoleEntry: (consoleEntry) => consoleEntries.push(consoleEntry),
}),
pwPreRequestModule({
envs: cloneDeep(envs),
handleSandboxResults: ({ envs }) => {
finalEnvs = envs
},
handleSandboxResults: ({ envs }) => (finalEnvs = envs),
}),
blobPolyfill,
ConsoleModule({
onLog(...args) {
console[args[0]](...args)
},
onCount(...args) {
console.count(args[0])
},
onTime(...args) {
console.timeEnd(args[0])
},
onTimeLog(...args) {
console.timeLog(...args)
},
onGroup(...args) {
console.group(...args)
},
onGroupEnd(...args) {
console.groupEnd(...args)
},
onClear(...args) {
console.clear(...args)
},
onAssert(...args) {
console.assert(...args)
},
onDir(...args) {
console.dir(...args)
},
onTable(...args) {
console.table(...args)
},
onFinish(entries) {
consoleEntries.push(...entries)
},
}),
crypto(),
esmModuleLoader,
fetch(),
esmModuleLoader,
])
if (result.type === "error") {
@ -100,14 +55,23 @@ export const runPreRequestScript = async (
typeof result.err === "object" &&
"message" in result.err
) {
return TE.left(`Script execution failed: ${result.err.message}`)()
return E.left(`Script execution failed: ${result.err.message}`)
}
return TE.left(`Script execution failed: ${String(result.err)}`)()
return E.left(`Script execution failed: ${String(result.err)}`)
}
return TE.right({
return E.right({
envs: finalEnvs,
consoleEntries,
})()
})
}
export const runPreRequestScript = async (
preRequestScript: string,
envs: TestResult["envs"],
experimentalScriptingSandbox = true
): Promise<E.Either<string, SandboxPreRequestResult>> =>
experimentalScriptingSandbox
? runPreRequestScriptWithFaradayCage(preRequestScript, envs)
: runPreRequestScriptWithWebWorker(preRequestScript, envs)

View file

@ -1,5 +1,10 @@
import { FaradayCage } from "faraday-cage"
import { ConsoleEntry } from "faraday-cage/modules"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import { defaultModules, pwPostRequestModule } from "~/cage-modules"
import { preventCyclicObjects } from "~/shared-utils"
import {
SandboxTestResult,
TestDescriptor,
@ -7,30 +12,34 @@ import {
TestResult,
} from "~/types"
import {
blobPolyfill,
ConsoleEntry,
console as ConsoleModule,
crypto,
esmModuleLoader,
fetch,
} from "faraday-cage/modules"
import { FaradayCage } from "faraday-cage"
import * as TE from "fp-ts/lib/TaskEither"
import { cloneDeep } from "lodash-es"
import { pwPostRequestModule } from "~/cage-modules/pw"
import { preventCyclicObjects } from "~/shared-utils"
import Worker from "./worker?worker&inline"
export const runTestScript = async (
const runTestScriptWithWebWorker = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
response: TestResponse
): Promise<E.Either<string, SandboxTestResult>> => {
return new Promise((resolve) => {
const worker = new Worker()
// Listen for the results from the web worker
worker.addEventListener("message", (event: MessageEvent) =>
resolve(event.data.results)
)
// Send the script to the web worker
worker.postMessage({
testScript,
envs,
response,
})
})
}
const runTestScriptWithFaradayCage = async (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
): Promise<E.Either<string, SandboxTestResult>> => {
const testRunStack: TestDescriptor[] = [
{ descriptor: "root", expectResults: [], children: [] },
@ -40,100 +49,56 @@ export const runTestScript = async (
let finalTestResults = testRunStack
const consoleEntries: ConsoleEntry[] = []
const responseObjHandle = preventCyclicObjects(response)
if (E.isLeft(responseObjHandle)) {
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)()
}
if (!experimentalScriptingSandbox) {
return new Promise((resolve) => {
const worker = new Worker()
// Listen for the results from the web worker
worker.addEventListener("message", (event: MessageEvent) =>
resolve(event.data.results)
)
// Send the script to the web worker
worker.postMessage({
testScript,
envs,
response,
})
})
}
const cage = await FaradayCage.create()
const result = await cage.runCode(testScript, [
...defaultModules({
handleConsoleEntry: (consoleEntry) => consoleEntries.push(consoleEntry),
}),
pwPostRequestModule({
envs: cloneDeep(envs),
testRunStack: cloneDeep(testRunStack),
response: responseObjHandle.right as TestResponse,
response,
handleSandboxResults: ({ envs, testRunStack }) => {
finalEnvs = envs
finalTestResults = testRunStack
},
}),
blobPolyfill,
ConsoleModule({
onLog(...args) {
console[args[0]](...args.slice(1))
},
onCount(...args) {
console.count(args[0])
},
onTime(...args) {
console.timeEnd(args[0])
},
onTimeLog(...args) {
console.timeLog(...args)
},
onGroup(...args) {
console.group(...args)
},
onGroupEnd(...args) {
console.groupEnd(...args)
},
onClear(...args) {
console.clear(...args)
},
onAssert(...args) {
console.assert(...args)
},
onDir(...args) {
console.dir(...args)
},
onTable(...args) {
console.table(...args)
},
onFinish(entries) {
consoleEntries.push(...entries)
},
}),
crypto(),
esmModuleLoader,
fetch(),
])
if (result.type === "error") {
if (result.type === "error") {
if (
result.err !== null &&
typeof result.err === "object" &&
"message" in result.err
) {
return TE.left(`Script execution failed: ${result.err.message}`)()
}
return TE.left(`Script execution failed: ${String(result.err)}`)()
if (
result.err !== null &&
typeof result.err === "object" &&
"message" in result.err
) {
return E.left(`Script execution failed: ${result.err.message}`)
}
return E.left(`Script execution failed: ${String(result.err)}`)
}
return TE.right(<SandboxTestResult>{
return E.right(<SandboxTestResult>{
tests: finalTestResults[0],
envs: finalEnvs,
consoleEntries,
})()
})
}
export const runTestScript = async (
testScript: string,
envs: TestResult["envs"],
response: TestResponse,
experimentalScriptingSandbox = true
): Promise<E.Either<string, SandboxTestResult>> => {
const responseObjHandle = preventCyclicObjects<TestResponse>(response)
if (E.isLeft(responseObjHandle)) {
return E.left(`Response marshalling failed: ${responseObjHandle.left}`)
}
return experimentalScriptingSandbox
? runTestScriptWithFaradayCage(testScript, envs, responseObjHandle.right)
: runTestScriptWithWebWorker(testScript, envs, responseObjHandle.right)
}

View file

@ -1133,8 +1133,8 @@ importers:
specifier: 4.17.12
version: 4.17.12
faraday-cage:
specifier: 0.0.15
version: 0.0.15
specifier: 0.0.16
version: 0.0.16
fp-ts:
specifier: 2.16.9
version: 2.16.9
@ -8859,8 +8859,8 @@ packages:
resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==}
engines: {node: ^12.20 || >= 14.13}
faraday-cage@0.0.15:
resolution: {integrity: sha512-ZERKHFsea2fZ0gfDVFOkXmtryhp+7CyUBFaa9nvRq7Gbf6XVnZ0f0dKsZjwoBzYqSD3Lhl2DFfK8bW4PEo1y0g==}
faraday-cage@0.0.16:
resolution: {integrity: sha512-+BHq/8LnqSsZ+qygj4+dgdis3iR+bBtB5dbWcw8ZNhdhoYQ+X9FS00qhQ6B6tIoEBNBHBSeQN3ydMXOjoE0y3Q==}
fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
@ -10938,6 +10938,7 @@ packages:
multer@1.4.5-lts.2:
resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==}
engines: {node: '>= 6.0.0'}
deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.
mute-stream@0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
@ -24439,7 +24440,7 @@ snapshots:
extract-files@11.0.0: {}
faraday-cage@0.0.15:
faraday-cage@0.0.16:
dependencies:
'@jitl/quickjs-ffi-types': 0.31.0
'@jitl/quickjs-singlefile-mjs-release-asyncify': 0.31.0