chore: migrate Node.js implementation for js-sandbox to isolated-vm (#3973)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
collectCoverage: true,
|
||||
setupFilesAfterEnv: ["./jest.setup.ts"],
|
||||
moduleNameMapper: {
|
||||
"~/(.*)": "<rootDir>/src/$1",
|
||||
"^lodash-es$": "lodash",
|
||||
},
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
require("@relmify/jest-fp-ts")
|
||||
4
packages/hoppscotch-js-sandbox/node.d.ts
vendored
4
packages/hoppscotch-js-sandbox/node.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
export { default } from "./dist/node.d.ts"
|
||||
export * from "./dist/node.d.ts"
|
||||
export { default } from "./dist/node/index.d.ts"
|
||||
export * from "./dist/node/index.d.ts"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
|
||||
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
|
||||
"test": "pnpm exec jest",
|
||||
"test": "vitest run",
|
||||
"build": "vite build && tsc --emitDeclarationOnly",
|
||||
"clean": "pnpm tsc --build --clean",
|
||||
"postinstall": "pnpm run build",
|
||||
@@ -69,10 +69,18 @@
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"io-ts": "2.2.16",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.8.4",
|
||||
"ts-jest": "27.1.5",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "5.0.5"
|
||||
"vite": "5.0.5",
|
||||
"vitest": "0.34.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"isolated-vm": "4.7.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"isolated-vm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
15
packages/hoppscotch-js-sandbox/setupFiles.ts
Normal file
15
packages/hoppscotch-js-sandbox/setupFiles.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Vitest doesn't work without globals
|
||||
// Ref: https://github.com/relmify/jest-fp-ts/issues/11
|
||||
|
||||
import decodeMatchers from "@relmify/jest-fp-ts/dist/decodeMatchers"
|
||||
import eitherMatchers from "@relmify/jest-fp-ts/dist/eitherMatchers"
|
||||
import optionMatchers from "@relmify/jest-fp-ts/dist/optionMatchers"
|
||||
import theseMatchers from "@relmify/jest-fp-ts/dist/theseMatchers"
|
||||
import eitherOrTheseMatchers from "@relmify/jest-fp-ts/dist/eitherOrTheseMatchers"
|
||||
import { expect } from "vitest"
|
||||
|
||||
expect.extend(decodeMatchers.matchers)
|
||||
expect.extend(eitherMatchers.matchers)
|
||||
expect.extend(optionMatchers.matchers)
|
||||
expect.extend(theseMatchers.matchers)
|
||||
expect.extend(eitherOrTheseMatchers.matchers)
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runPreRequestScript } from "~/pre-request/node-vm"
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runPreRequestScript, runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
describe("Base64 helper functions", () => {
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -23,7 +24,7 @@ describe("toBe", () => {
|
||||
return expect(
|
||||
func(
|
||||
`
|
||||
pw.expect(2).toBe(2)
|
||||
pw.expect(2).toBe(2)
|
||||
`,
|
||||
fakeResponse
|
||||
)()
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runPreRequestScript } from "~/pre-request/node-vm"
|
||||
import { runPreRequestScript } from "~/node"
|
||||
|
||||
describe("runPreRequestScript", () => {
|
||||
test("returns the updated environment properly", () => {
|
||||
@@ -1,4 +1,6 @@
|
||||
import { preventCyclicObjects } from "~/utils"
|
||||
import { preventCyclicObjects } from "~/shared-utils"
|
||||
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
describe("preventCyclicObjects", () => {
|
||||
test("succeeds with a simple object", () => {
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { runTestScript } from "~/node"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./pre-request/node-vm"
|
||||
export * from "./test-runner/node-vm"
|
||||
2
packages/hoppscotch-js-sandbox/src/node/index.ts
Normal file
2
packages/hoppscotch-js-sandbox/src/node/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { runPreRequestScript } from "./pre-request"
|
||||
export { runTestScript } from "./test-runner"
|
||||
91
packages/hoppscotch-js-sandbox/src/node/pre-request.ts
Normal file
91
packages/hoppscotch-js-sandbox/src/node/pre-request.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import { createRequire } from "module"
|
||||
|
||||
import type ivmT from "isolated-vm"
|
||||
|
||||
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"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> =>
|
||||
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}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
217
packages/hoppscotch-js-sandbox/src/node/test-runner.ts
Normal file
217
packages/hoppscotch-js-sandbox/src/node/test-runner.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { createRequire } from "module"
|
||||
|
||||
import type ivmT from "isolated-vm"
|
||||
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import {
|
||||
getTestRunnerScriptMethods,
|
||||
preventCyclicObjects,
|
||||
} from "~/shared-utils"
|
||||
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
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
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}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse,
|
||||
isolate: ivmT.Isolate,
|
||||
context: ivmT.Context
|
||||
): Promise<TestResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Parse response object
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return reject(`Response parsing failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
const jail = context.global
|
||||
|
||||
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
|
||||
|
||||
const serializedAPIMethods = getSerializedAPIMethods({
|
||||
...pw,
|
||||
response: responseObjHandle.right,
|
||||
})
|
||||
jail.setSync("serializedAPIMethods", serializedAPIMethods, { copy: true })
|
||||
|
||||
jail.setSync("atob", atob)
|
||||
jail.setSync("btoa", btoa)
|
||||
|
||||
jail.setSync("ivm", ivm)
|
||||
|
||||
// Methods in the isolate context can't be invoked straightaway
|
||||
const finalScript = `
|
||||
const pw = new Proxy(serializedAPIMethods, {
|
||||
get: (pwObj, pwObjProp) => {
|
||||
// pw.expect(), pw.env, etc.
|
||||
const topLevelEntry = pwObj[pwObjProp]
|
||||
|
||||
// If the entry exists and is a function
|
||||
// pw.expect(), pw.test(), etc.
|
||||
if (topLevelEntry && topLevelEntry.typeof === "function") {
|
||||
// pw.test() just involves invoking the function via "applySync()"
|
||||
if (pwObjProp === "test") {
|
||||
return (...args) => topLevelEntry.applySync(null, args)
|
||||
}
|
||||
|
||||
// pw.expect() returns an object with matcher methods
|
||||
return (...args) => {
|
||||
// Invoke "pw.expect()" and get access to the object with matcher methods
|
||||
const expectFnResult = topLevelEntry.applySync(
|
||||
null,
|
||||
args.map((expectVal) => {
|
||||
if (typeof expectVal === "object") {
|
||||
if (expectVal === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Only arrays and objects stringified here should be parsed from the "pw.expect()" method definition
|
||||
// The usecase is that any JSON string supplied should be preserved
|
||||
// An extra "isStringifiedWithinIsolate" prop is added to indicate it has to be parsed
|
||||
|
||||
if (Array.isArray(expectVal)) {
|
||||
return JSON.stringify({
|
||||
arr: expectVal,
|
||||
isStringifiedWithinIsolate: true,
|
||||
})
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
...expectVal,
|
||||
isStringifiedWithinIsolate: true,
|
||||
})
|
||||
}
|
||||
|
||||
return expectVal
|
||||
})
|
||||
)
|
||||
|
||||
// Matcher methods that can be chained with "pw.expect()"
|
||||
// pw.expect().toBe(), etc
|
||||
if (expectFnResult.typeof === "object") {
|
||||
// Access the getter that points to the negated matcher methods via "{ accessors: true }"
|
||||
const matcherMethods = {
|
||||
not: expectFnResult.getSync("not", { accessors: true }),
|
||||
}
|
||||
|
||||
// Serialize matcher methods for use in the isolate context
|
||||
const matcherMethodNames = [
|
||||
"toBe",
|
||||
"toBeLevel2xx",
|
||||
"toBeLevel3xx",
|
||||
"toBeLevel4xx",
|
||||
"toBeLevel5xx",
|
||||
"toBeType",
|
||||
"toHaveLength",
|
||||
"toInclude",
|
||||
]
|
||||
matcherMethodNames.forEach((methodName) => {
|
||||
matcherMethods[methodName] = expectFnResult.getSync(methodName)
|
||||
})
|
||||
|
||||
return new Proxy(matcherMethods, {
|
||||
get: (matcherMethodTarget, matcherMethodProp) => {
|
||||
// pw.expect().not.toBe(), etc
|
||||
const matcherMethodEntry = matcherMethodTarget[matcherMethodProp]
|
||||
|
||||
if (matcherMethodProp === "not") {
|
||||
return new Proxy(matcherMethodEntry, {
|
||||
get: (negatedObjTarget, negatedObjprop) => {
|
||||
// Return the negated matcher method defn that is invoked from the test script
|
||||
const negatedMatcherMethodDefn = negatedObjTarget.getSync(negatedObjprop)
|
||||
return negatedMatcherMethodDefn
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Return the matcher method defn that is invoked from the test script
|
||||
return matcherMethodEntry
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "pw.env" set of API methods
|
||||
if (typeof topLevelEntry === "object" && pwObjProp !== "response") {
|
||||
return new Proxy(topLevelEntry, {
|
||||
get: (subTarget, subProp) => {
|
||||
const subLevelProperty = subTarget[subProp]
|
||||
if (
|
||||
subLevelProperty &&
|
||||
subLevelProperty.typeof === "function"
|
||||
) {
|
||||
return (...args) => subLevelProperty.applySync(null, args)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return topLevelEntry
|
||||
},
|
||||
})
|
||||
|
||||
${testScript}
|
||||
`
|
||||
|
||||
// Create a script and compile it
|
||||
const script = isolate.compileScript(finalScript)
|
||||
|
||||
// Run the test script in the provided context
|
||||
script
|
||||
.then((script) => script.run(context))
|
||||
.then(() => {
|
||||
resolve({
|
||||
tests: testRunStack,
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
23
packages/hoppscotch-js-sandbox/src/node/utils.ts
Normal file
23
packages/hoppscotch-js-sandbox/src/node/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRequire } from "module"
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
const ivm = nodeRequire("isolated-vm")
|
||||
|
||||
// Helper function to recursively wrap methods in `ivm.Reference`
|
||||
export const getSerializedAPIMethods = (
|
||||
namespaceObj: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(namespaceObj)) {
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
result[key] = getSerializedAPIMethods(value as Record<string, unknown>)
|
||||
} else if (typeof value === "function") {
|
||||
result[key] = new ivm.Reference(value)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import { createContext, runInContext } from "vm"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/utils"
|
||||
|
||||
export const runPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
return createContext()
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((context) =>
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
// Expose pw to the context
|
||||
context.pw = pw
|
||||
context.atob = atob
|
||||
context.btoa = btoa
|
||||
|
||||
// Run the pre-request script in the provided context
|
||||
runInContext(preRequestScript, context)
|
||||
|
||||
resolve(updatedEnvs)
|
||||
}),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -182,6 +182,36 @@ const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getResolvedExpectValue = (expectVal: any) => {
|
||||
if (typeof expectVal !== "string") {
|
||||
return expectVal
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedExpectVal = JSON.parse(expectVal)
|
||||
|
||||
// Supplying non-primitive values is not permitted in the `isStringifiedWithinIsolate` property indicates that the object was stringified before executing the script from the isolate context
|
||||
// This is done to ensure a JSON string supplied as the "expectVal" is not parsed and preserved as is
|
||||
if (typeof parsedExpectVal === "object") {
|
||||
if (parsedExpectVal.isStringifiedWithinIsolate !== true) {
|
||||
return expectVal
|
||||
}
|
||||
|
||||
// For an array, the contents are stored in the `arr` property
|
||||
if (Array.isArray(parsedExpectVal.arr)) {
|
||||
return parsedExpectVal.arr
|
||||
}
|
||||
|
||||
delete parsedExpectVal.isStringifiedWithinIsolate
|
||||
return parsedExpectVal
|
||||
}
|
||||
|
||||
return expectVal
|
||||
} catch (_) {
|
||||
return expectVal
|
||||
}
|
||||
}
|
||||
|
||||
export function preventCyclicObjects(
|
||||
obj: Record<string, any>
|
||||
): E.Left<string> | E.Right<Record<string, any>> {
|
||||
@@ -215,15 +245,18 @@ export const createExpectation = (
|
||||
) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
// Non-primitive values supplied are stringified in the isolate context
|
||||
const resolvedExpectVal = getResolvedExpectValue(expectVal)
|
||||
|
||||
const toBeFn = (expectedVal: any) => {
|
||||
let assertion = expectVal === expectedVal
|
||||
let assertion = resolvedExpectVal === expectedVal
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
const message = `Expected '${resolvedExpectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`
|
||||
|
||||
@@ -240,7 +273,7 @@ export const createExpectation = (
|
||||
rangeStart: number,
|
||||
rangeEnd: number
|
||||
) => {
|
||||
const parsedExpectVal = parseInt(expectVal)
|
||||
const parsedExpectVal = parseInt(resolvedExpectVal)
|
||||
|
||||
if (!Number.isNaN(parsedExpectVal)) {
|
||||
let assertion =
|
||||
@@ -260,7 +293,7 @@ export const createExpectation = (
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = `Expected ${level}-level status but could not parse value '${expectVal}'`
|
||||
const message = `Expected ${level}-level status but could not parse value '${resolvedExpectVal}'`
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
@@ -288,14 +321,14 @@ export const createExpectation = (
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
let assertion = typeof resolvedExpectVal === expectedType
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
const message = `Expected '${resolvedExpectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`
|
||||
|
||||
@@ -316,7 +349,12 @@ export const createExpectation = (
|
||||
}
|
||||
|
||||
const toHaveLengthFn = (expectedLength: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(resolvedExpectVal) ||
|
||||
typeof resolvedExpectVal === "string"
|
||||
)
|
||||
) {
|
||||
const message =
|
||||
"Expected toHaveLength to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
@@ -328,7 +366,7 @@ export const createExpectation = (
|
||||
}
|
||||
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = expectVal.length === expectedLength
|
||||
let assertion = resolvedExpectVal.length === expectedLength
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
@@ -355,7 +393,12 @@ export const createExpectation = (
|
||||
}
|
||||
|
||||
const toIncludeFn = (needle: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(resolvedExpectVal) ||
|
||||
typeof resolvedExpectVal === "string"
|
||||
)
|
||||
) {
|
||||
const message = "Expected toInclude to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
@@ -382,13 +425,13 @@ export const createExpectation = (
|
||||
return undefined
|
||||
}
|
||||
|
||||
let assertion = expectVal.includes(needle)
|
||||
let assertion = resolvedExpectVal.includes(needle)
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const expectValPretty = JSON.stringify(expectVal)
|
||||
const expectValPretty = JSON.stringify(resolvedExpectVal)
|
||||
const needlePretty = JSON.stringify(needle)
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected ${expectValPretty} to${
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { createContext, runInContext } from "vm"
|
||||
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
|
||||
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => {
|
||||
return createContext()
|
||||
},
|
||||
(reason) => `Context initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((context) =>
|
||||
TE.tryCatch(
|
||||
() => executeScriptInContext(testScript, envs, response, context),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse,
|
||||
context: any
|
||||
): Promise<TestResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Parse response object
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return reject(`Response parsing failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
|
||||
|
||||
// Expose pw to the context
|
||||
context.pw = { ...pw, response: responseObjHandle.right }
|
||||
context.atob = atob
|
||||
context.btoa = btoa
|
||||
|
||||
// Run the test script in the provided context
|
||||
runInContext(testScript, context)
|
||||
|
||||
resolve({
|
||||
tests: testRunStack,
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./pre-request/web-worker"
|
||||
export * from "./test-runner/web-worker"
|
||||
2
packages/hoppscotch-js-sandbox/src/web/index.ts
Normal file
2
packages/hoppscotch-js-sandbox/src/web/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { runPreRequestScript } from "./pre-request"
|
||||
export { runTestScript } from "./test-runner"
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/utils"
|
||||
import { getPreRequestScriptMethods } from "~/shared-utils"
|
||||
|
||||
const executeScriptInContext = (
|
||||
preRequestScript: string,
|
||||
@@ -2,7 +2,10 @@ import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils"
|
||||
import {
|
||||
getTestRunnerScriptMethods,
|
||||
preventCyclicObjects,
|
||||
} from "~/shared-utils"
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
@@ -7,16 +7,20 @@ export default defineConfig({
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: {
|
||||
web: "./src/web.ts",
|
||||
node: "./src/node.ts",
|
||||
web: "./src/web/index.ts",
|
||||
node: "./src/node/index.ts",
|
||||
},
|
||||
name: "js-sandbox",
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vm"],
|
||||
external: ["module"],
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "node",
|
||||
setupFiles: ["./setupFiles.ts"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": resolve(__dirname, "./src"),
|
||||
|
||||
4
packages/hoppscotch-js-sandbox/web.d.ts
vendored
4
packages/hoppscotch-js-sandbox/web.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
export { default } from "./dist/web.d.ts"
|
||||
export * from "./dist/web.d.ts"
|
||||
export { default } from "./dist/web/index.d.ts"
|
||||
export * from "./dist/web/index.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user