refactor(scripting-revamp): migrate js-sandbox to web worker/Node vm based implementation (#3619)
This commit is contained in:
@@ -147,7 +147,7 @@ module.exports = {
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/src/__tests__/**/*.*.ts",
|
||||
"**/src/__tests__/commands/**/*.*.ts",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
"debugger": "node debugger.js 9999",
|
||||
"prepublish": "pnpm exec tsup",
|
||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
||||
"test": "pnpm run build && jest && rm -rf dist",
|
||||
"do-typecheck": "pnpm exec tsc --noEmit",
|
||||
"test": "pnpm run build && jest && rm -rf dist"
|
||||
"do-test": "pnpm test"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
|
||||
|
||||
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
|
||||
});
|
||||
|
||||
|
||||
test("Malformed collection file.", async () => {
|
||||
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
|
||||
"malformed-collection2.json"
|
||||
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
||||
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||
const { error } = await execAsync(cmd);
|
||||
const { error, stdout } = await execAsync(cmd);
|
||||
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
@@ -129,7 +129,6 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||
const { stderr } = await execAsync(cmd);
|
||||
const out = getErrorCode(stderr);
|
||||
console.log("invalid value thing", out)
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"URL": "https://echo.hoppscotch.io",
|
||||
"HOST": "echo.hoppscotch.io",
|
||||
"X-COUNTRY": "IN",
|
||||
"BODY_VALUE": "body_value",
|
||||
"BODY_KEY": "body_key"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"method": "POST",
|
||||
"auth": { "authType": "none", "authActive": true },
|
||||
"preRequestScript": "",
|
||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||
"body": {
|
||||
"contentType": "application/json",
|
||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||
|
||||
@@ -6,23 +6,24 @@ import {
|
||||
parseTemplateString,
|
||||
parseTemplateStringE,
|
||||
} from "@hoppscotch/data";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as E from "fp-ts/Either";
|
||||
import * as O from "fp-ts/Option";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as S from "fp-ts/string";
|
||||
import qs from "qs";
|
||||
|
||||
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { isHoppCLIError } from "./checks";
|
||||
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
|
||||
import { toFormData } from "./mutators";
|
||||
import { getEffectiveFinalMetaData } from "./getters";
|
||||
import { PreRequestMetrics } from "../types/response";
|
||||
import { isHoppCLIError } from "./checks";
|
||||
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||
import { getEffectiveFinalMetaData } from "./getters";
|
||||
import { toFormData } from "./mutators";
|
||||
|
||||
/**
|
||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||
import { hrtime } from "process";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import { TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/node";
|
||||
import * as A from "fp-ts/Array";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import * as RA from "fp-ts/ReadonlyArray";
|
||||
import * as T from "fp-ts/Task";
|
||||
import * as TE from "fp-ts/TaskEither";
|
||||
import { flow, pipe } from "fp-ts/function";
|
||||
import { hrtime } from "process";
|
||||
|
||||
import {
|
||||
RequestRunnerResponse,
|
||||
TestReport,
|
||||
TestScriptParams,
|
||||
} from "../interfaces/response";
|
||||
import { error, HoppCLIError } from "../types/errors";
|
||||
import { HoppCLIError, error } from "../types/errors";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||
import { getDurationInSeconds } from "./getters";
|
||||
@@ -36,7 +38,7 @@ export const testRunner = (
|
||||
pipe(
|
||||
TE.of(testScriptData),
|
||||
TE.chain(({ testScript, response, envs }) =>
|
||||
execTestScript(testScript, envs, response)
|
||||
runTestScript(testScript, envs, response)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { Observable, Subject } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import {
|
||||
SandboxTestResult,
|
||||
runTestScript,
|
||||
TestDescriptor,
|
||||
} from "@hoppscotch/js-sandbox"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import { Ref } from "vue"
|
||||
|
||||
import {
|
||||
environmentsStore,
|
||||
getCurrentEnvironment,
|
||||
@@ -29,9 +18,18 @@ import {
|
||||
setGlobalEnvVariables,
|
||||
updateEnvironment,
|
||||
} from "~/newstore/environments"
|
||||
import { Ref } from "vue"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import {
|
||||
getCombinedEnvVariables,
|
||||
getFinalEnvsFromPreRequest,
|
||||
} from "./preRequest"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -89,7 +87,7 @@ export function runRESTRequest$(
|
||||
const res = getFinalEnvsFromPreRequest(
|
||||
tab.value.document.request.preRequestScript,
|
||||
getCombinedEnvVariables()
|
||||
)().then((envs) => {
|
||||
).then((envs) => {
|
||||
if (cancelCalled) return E.left("cancellation" as const)
|
||||
|
||||
if (E.isLeft(envs)) {
|
||||
@@ -125,7 +123,7 @@ export function runRESTRequest$(
|
||||
body: getTestableBody(res),
|
||||
headers: res.headers,
|
||||
}
|
||||
)()
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||
|
||||
export const getCombinedEnvVariables = () => ({
|
||||
global: cloneDeep(getGlobalVariables()),
|
||||
@@ -17,4 +20,5 @@ export const getFinalEnvsFromPreRequest = (
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
) => runPreRequestScript(script, envs)
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
runPreRequestScript(script, envs)
|
||||
|
||||
2
packages/hoppscotch-js-sandbox/index.d.ts
vendored
Normal file
2
packages/hoppscotch-js-sandbox/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/types/index.d.ts"
|
||||
export * from "./dist/types/index.d.ts"
|
||||
@@ -1,6 +1,10 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
collectCoverage: true,
|
||||
setupFilesAfterEnv: ["./jest.setup.ts"],
|
||||
moduleNameMapper: {
|
||||
"~/(.*)": "<rootDir>/src/$1",
|
||||
"^lodash-es$": "lodash",
|
||||
},
|
||||
}
|
||||
|
||||
2
packages/hoppscotch-js-sandbox/node.d.ts
vendored
Normal file
2
packages/hoppscotch-js-sandbox/node.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/node.d.ts"
|
||||
export * from "./dist/node.d.ts"
|
||||
@@ -2,16 +2,27 @@
|
||||
"name": "@hoppscotch/js-sandbox",
|
||||
"version": "2.1.0",
|
||||
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
|
||||
"main": "./lib/index.js",
|
||||
"module": "./lib/index.mjs",
|
||||
"type": "commonjs",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./lib/index.js",
|
||||
"default": "./lib/index.mjs"
|
||||
"types": "./dist/types/index.d.ts"
|
||||
},
|
||||
"./web": {
|
||||
"import": "./dist/web.js",
|
||||
"require": "./dist/web.cjs",
|
||||
"types": "./dist/web.d.ts"
|
||||
},
|
||||
"./node": {
|
||||
"import": "./dist/node.js",
|
||||
"require": "./dist/node.cjs",
|
||||
"types": "./dist/node.d.ts"
|
||||
}
|
||||
},
|
||||
"types": "./lib/",
|
||||
"types": "./index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=3"
|
||||
@@ -20,7 +31,7 @@
|
||||
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
|
||||
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
|
||||
"test": "pnpm exec jest",
|
||||
"build": "pnpm exec tsup",
|
||||
"build": "vite build && tsc --emitDeclarationOnly",
|
||||
"clean": "pnpm tsc --build --clean",
|
||||
"postinstall": "pnpm run build",
|
||||
"prepublish": "pnpm run build",
|
||||
@@ -41,10 +52,10 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"fp-ts": "^2.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"quickjs-emscripten": "^0.15.0",
|
||||
"tsup": "^5.12.5"
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "^3.1.2",
|
||||
@@ -61,6 +72,7 @@
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.8.4",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { execPreRequestScript } from "../preRequest"
|
||||
import { runPreRequestScript } from "~/pre-request/node-vm"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
describe("execPreRequestScript", () => {
|
||||
test("returns the updated envirionment properly", () => {
|
||||
return expect(
|
||||
execPreRequestScript(
|
||||
runPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob", "newbob")
|
||||
`,
|
||||
@@ -27,7 +27,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails if the key is not a string", () => {
|
||||
return expect(
|
||||
execPreRequestScript(
|
||||
runPreRequestScript(
|
||||
`
|
||||
pw.env.set(10, "newbob")
|
||||
`,
|
||||
@@ -44,7 +44,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails if the value is not a string", () => {
|
||||
return expect(
|
||||
execPreRequestScript(
|
||||
runPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob", 10)
|
||||
`,
|
||||
@@ -61,7 +61,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("fails for invalid syntax", () => {
|
||||
return expect(
|
||||
execPreRequestScript(
|
||||
runPreRequestScript(
|
||||
`
|
||||
pw.env.set("bob",
|
||||
`,
|
||||
@@ -78,7 +78,7 @@ describe("execPreRequestScript", () => {
|
||||
|
||||
test("creates new env variable if doesn't exist", () => {
|
||||
return expect(
|
||||
execPreRequestScript(
|
||||
runPreRequestScript(
|
||||
`
|
||||
pw.env.set("foo", "bar")
|
||||
`,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
import "@relmify/jest-fp-ts"
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
import "@relmify/jest-fp-ts"
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse, TestResult } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,13 +12,13 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.envs)
|
||||
)
|
||||
|
||||
const funcTest = (script: string, envs: TestResult["envs"]) =>
|
||||
pipe(
|
||||
execTestScript(script, envs, fakeResponse),
|
||||
runTestScript(script, envs, fakeResponse),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "@relmify/jest-fp-ts"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
import "@relmify/jest-fp-ts"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { execTestScript, TestResponse } from "../../test-runner"
|
||||
|
||||
import { runTestScript } from "~/test-runner/node-vm"
|
||||
import { TestResponse } from "~/types"
|
||||
|
||||
const fakeResponse: TestResponse = {
|
||||
status: 200,
|
||||
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
|
||||
|
||||
const func = (script: string, res: TestResponse) =>
|
||||
pipe(
|
||||
execTestScript(script, { global: [], selected: [] }, res),
|
||||
runTestScript(script, { global: [], selected: [] }, res),
|
||||
TE.map((x) => x.tests)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
import { match } from "fp-ts/lib/Either"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import * as QuickJS from "quickjs-emscripten"
|
||||
import { marshalObjectToVM } from "../utils"
|
||||
import { preventCyclicObjects } from "~/utils"
|
||||
|
||||
let vm: QuickJS.QuickJSVm
|
||||
|
||||
beforeAll(async () => {
|
||||
const qjs = await QuickJS.getQuickJS()
|
||||
vm = qjs.createVm()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vm.dispose()
|
||||
})
|
||||
|
||||
describe("marshalObjectToVM", () => {
|
||||
test("successfully marshals simple object into the vm", () => {
|
||||
describe("preventCyclicObjects", () => {
|
||||
test("succeeds with a simple object", () => {
|
||||
const testObj = {
|
||||
a: 1,
|
||||
}
|
||||
|
||||
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
|
||||
marshalObjectToVM(vm, testObj),
|
||||
match(
|
||||
() => null,
|
||||
(result) => result
|
||||
)
|
||||
)
|
||||
|
||||
expect(objVMHandle).not.toBeNull()
|
||||
expect(vm.dump(objVMHandle!)).toEqual(testObj)
|
||||
|
||||
objVMHandle!.dispose()
|
||||
expect(preventCyclicObjects(testObj)).toBeRight()
|
||||
})
|
||||
|
||||
test("fails marshalling cyclic object into vm", () => {
|
||||
test("fails with a cyclic object", () => {
|
||||
const testObj = {
|
||||
a: 1,
|
||||
b: null as any,
|
||||
@@ -42,14 +17,6 @@ describe("marshalObjectToVM", () => {
|
||||
|
||||
testObj.b = testObj
|
||||
|
||||
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
|
||||
marshalObjectToVM(vm, testObj),
|
||||
match(
|
||||
() => null,
|
||||
(result) => result
|
||||
)
|
||||
)
|
||||
|
||||
expect(objVMHandle).toBeNull()
|
||||
expect(preventCyclicObjects(testObj)).toBeLeft()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { execPreRequestScript } from "./preRequest"
|
||||
import {
|
||||
execTestScript,
|
||||
TestResponse as _TestResponse,
|
||||
TestDescriptor as _TestDescriptor,
|
||||
TestResult,
|
||||
} from "./test-runner"
|
||||
|
||||
export * from "./test-runner"
|
||||
|
||||
export type TestResponse = _TestResponse
|
||||
export type TestDescriptor = _TestDescriptor
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
|
||||
|
||||
/**
|
||||
* Executes a given test script on the test-runner sandbox
|
||||
* @param testScript The string of the script to run
|
||||
* @returns A TaskEither with an error message or a TestDescriptor with the final status
|
||||
*/
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
) =>
|
||||
pipe(
|
||||
execTestScript(testScript, envs, response),
|
||||
TE.chain((results) =>
|
||||
TE.right(<SandboxTestResult>{
|
||||
envs: results.envs,
|
||||
tests: results.tests[0],
|
||||
})
|
||||
) // execTestScript returns an array of descriptors with a single element (extract that)
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes a given pre-request script on the sandbox
|
||||
* @param preRequestScript The script to run
|
||||
* @param env The environment variables active
|
||||
* @returns A TaskEither with an error message or an array of the final environments with the all the script values applied
|
||||
*/
|
||||
export const runPreRequestScript = execPreRequestScript
|
||||
2
packages/hoppscotch-js-sandbox/src/node.ts
Normal file
2
packages/hoppscotch-js-sandbox/src/node.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./pre-request/node-vm"
|
||||
export * from "./test-runner/node-vm"
|
||||
@@ -0,0 +1,36 @@
|
||||
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
|
||||
|
||||
// Run the pre-request script in the provided context
|
||||
runInContext(preRequestScript, context)
|
||||
|
||||
resolve(updatedEnvs)
|
||||
}),
|
||||
(reason) => `Script execution failed: ${reason}`
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): Promise<E.Either<string, TestResult["envs"]>> =>
|
||||
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,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
|
||||
import { TestResult } from "~/types"
|
||||
import { getPreRequestScriptMethods } from "~/utils"
|
||||
|
||||
const executeScriptInContext = (
|
||||
preRequestScript: string,
|
||||
envs: TestResult["envs"]
|
||||
): TE.TaskEither<string, TestResult["envs"]> => {
|
||||
try {
|
||||
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
|
||||
|
||||
// Create a function from the pre request script using the `Function` constructor
|
||||
const executeScript = new Function("pw", preRequestScript)
|
||||
|
||||
// Execute the script
|
||||
executeScript(pw)
|
||||
|
||||
return TE.right(updatedEnvs)
|
||||
} catch (error) {
|
||||
return TE.left(`Script execution failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { preRequestScript, envs } = event.data
|
||||
|
||||
const results = await executeScriptInContext(preRequestScript, envs)()
|
||||
|
||||
// Post the result back to the main thread
|
||||
self.postMessage({ results })
|
||||
})
|
||||
@@ -1,166 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/lib/TaskEither"
|
||||
import * as qjs from "quickjs-emscripten"
|
||||
import cloneDeep from "lodash/clone"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { getEnv, setEnv } from "./utils"
|
||||
|
||||
type Envs = {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
|
||||
export const execPreRequestScript = (
|
||||
preRequestScript: string,
|
||||
envs: Envs
|
||||
): TE.TaskEither<string, Envs> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => await qjs.getQuickJS(),
|
||||
(reason) => `QuickJS initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain((QuickJS) => {
|
||||
let currentEnvs = cloneDeep(envs)
|
||||
|
||||
const vm = QuickJS.createVm()
|
||||
|
||||
const pwHandle = vm.newObject()
|
||||
|
||||
// Environment management APIs
|
||||
// TODO: Unified Implementation
|
||||
const envHandle = vm.newObject()
|
||||
|
||||
const envGetHandle = vm.newFunction("get", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
O.match(
|
||||
() => vm.undefined,
|
||||
({ value }) => vm.newString(value)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envGetResolveHandle = vm.newFunction("getResolve", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [...envs.selected, ...envs.global]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
|
||||
// Create a new VM String
|
||||
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
|
||||
E.map((x) => vm.newString(x)),
|
||||
|
||||
E.getOrElse(() => vm.undefined)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
currentEnvs = setEnv(key, value, currentEnvs)
|
||||
|
||||
return {
|
||||
value: vm.undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...currentEnvs.selected,
|
||||
...currentEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return {
|
||||
value: vm.newString(result),
|
||||
}
|
||||
})
|
||||
|
||||
vm.setProp(envHandle, "resolve", envResolveHandle)
|
||||
envResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "set", envSetHandle)
|
||||
envSetHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
|
||||
envGetResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "get", envGetHandle)
|
||||
envGetHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "env", envHandle)
|
||||
envHandle.dispose()
|
||||
|
||||
vm.setProp(vm.global, "pw", pwHandle)
|
||||
pwHandle.dispose()
|
||||
|
||||
const evalRes = vm.evalCode(preRequestScript)
|
||||
|
||||
if (evalRes.error) {
|
||||
const errorData = vm.dump(evalRes.error)
|
||||
evalRes.error.dispose()
|
||||
|
||||
return TE.left(errorData)
|
||||
}
|
||||
|
||||
vm.dispose()
|
||||
|
||||
return TE.right(currentEnvs)
|
||||
})
|
||||
)
|
||||
@@ -1,610 +0,0 @@
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as qjs from "quickjs-emscripten"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import { getEnv, marshalObjectToVM, setEnv } from "./utils"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
export type TestResponse = {
|
||||
/** Status Code of the response */
|
||||
status: number
|
||||
/** List of headers returned */
|
||||
headers: { key: string; value: string }[]
|
||||
/**
|
||||
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
|
||||
*/
|
||||
body: string | object
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an expectation statement
|
||||
*/
|
||||
type ExpectResult = { status: "pass" | "fail" | "error"; message: string } // The expectation failed (fail) or errored (error)
|
||||
|
||||
/**
|
||||
* An object defining the result of the execution of a
|
||||
* test block
|
||||
*/
|
||||
export type TestDescriptor = {
|
||||
/**
|
||||
* The name of the test block
|
||||
*/
|
||||
descriptor: string
|
||||
|
||||
/**
|
||||
* Expectation results of the test block
|
||||
*/
|
||||
expectResults: ExpectResult[]
|
||||
|
||||
/**
|
||||
* Children test blocks (test blocks inside the test block)
|
||||
*/
|
||||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution
|
||||
*/
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Expectation object for use inside the sandbox
|
||||
* @param vm The QuickJS sandbox VM instance
|
||||
* @param expectVal The expecting value of the expectation
|
||||
* @param negated Whether the expectation is negated (negative)
|
||||
* @param currTestStack The current state of the test execution stack
|
||||
* @returns Handle to the expectation object in VM
|
||||
*/
|
||||
function createExpectation(
|
||||
vm: qjs.QuickJSVm,
|
||||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
): qjs.QuickJSHandle {
|
||||
const resultHandle = vm.newObject()
|
||||
|
||||
const toBeFnHandle = vm.newFunction("toBe", (expectedValHandle) => {
|
||||
const expectedVal = vm.dump(expectedValHandle)
|
||||
|
||||
let assertion = expectVal === expectedVal
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel2xxHandle = vm.newFunction("toBeLevel2xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 200 && expectVal <= 299
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 200-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 200-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 200-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel3xxHandle = vm.newFunction("toBeLevel3xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 300 && expectVal <= 399
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 300-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 300-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 300-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel4xxHandle = vm.newFunction("toBeLevel4xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 400 && expectVal <= 499
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 400-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 400-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 400-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeLevel5xxHandle = vm.newFunction("toBeLevel5xx", () => {
|
||||
// Check if the expected value is a number, else it is an error
|
||||
if (typeof expectVal === "number" && !Number.isNaN(expectVal)) {
|
||||
let assertion = expectVal >= 500 && expectVal <= 599
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 500-level status`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be 500-level status`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected 500-level status but could not parse value '${expectVal}'`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toBeTypeHandle = vm.newFunction("toBeType", (expectedValHandle) => {
|
||||
const expectedType = vm.dump(expectedValHandle)
|
||||
|
||||
// Check if the expectation param is a valid type name string, else error
|
||||
if (
|
||||
[
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
const toHaveLengthHandle = vm.newFunction(
|
||||
"toHaveLength",
|
||||
(expectedValHandle) => {
|
||||
const expectedLength = vm.dump(expectedValHandle)
|
||||
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected toHaveLength to be called for an array or string`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
// Check if the parameter is a number, else error
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = (expectVal as any[]).length === expectedLength
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toHaveLength should be a number`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
)
|
||||
|
||||
const toIncludeHandle = vm.newFunction("toInclude", (needleHandle) => {
|
||||
const expectedVal = vm.dump(needleHandle)
|
||||
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Expected toInclude to be called for an array or string`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
if (expectedVal === null) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toInclude should not be null`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
if (expectedVal === undefined) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message: `Argument for toInclude should not be undefined`,
|
||||
})
|
||||
|
||||
return { value: vm.undefined }
|
||||
}
|
||||
|
||||
let assertion = expectVal.includes(expectedVal)
|
||||
if (negated) assertion = !assertion
|
||||
|
||||
const expectValPretty = JSON.stringify(expectVal)
|
||||
const expectedValPretty = JSON.stringify(expectedVal)
|
||||
|
||||
if (assertion) {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${expectedValPretty}`,
|
||||
})
|
||||
} else {
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${expectedValPretty}`,
|
||||
})
|
||||
}
|
||||
|
||||
return { value: vm.undefined }
|
||||
})
|
||||
|
||||
vm.setProp(resultHandle, "toBe", toBeFnHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel2xx", toBeLevel2xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel3xx", toBeLevel3xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel4xx", toBeLevel4xxHandle)
|
||||
vm.setProp(resultHandle, "toBeLevel5xx", toBeLevel5xxHandle)
|
||||
vm.setProp(resultHandle, "toBeType", toBeTypeHandle)
|
||||
vm.setProp(resultHandle, "toHaveLength", toHaveLengthHandle)
|
||||
vm.setProp(resultHandle, "toInclude", toIncludeHandle)
|
||||
|
||||
vm.defineProp(resultHandle, "not", {
|
||||
get: () => {
|
||||
return createExpectation(vm, expectVal, !negated, currTestStack)
|
||||
},
|
||||
})
|
||||
|
||||
toBeFnHandle.dispose()
|
||||
toBeLevel2xxHandle.dispose()
|
||||
toBeLevel3xxHandle.dispose()
|
||||
toBeLevel4xxHandle.dispose()
|
||||
toBeLevel5xxHandle.dispose()
|
||||
toBeTypeHandle.dispose()
|
||||
toHaveLengthHandle.dispose()
|
||||
toIncludeHandle.dispose()
|
||||
|
||||
return resultHandle
|
||||
}
|
||||
|
||||
export const execTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, TestResult> =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
async () => await qjs.getQuickJS(),
|
||||
(reason) => `QuickJS initialization failed: ${reason}`
|
||||
),
|
||||
TE.chain(
|
||||
// TODO: Make this more functional ?
|
||||
(QuickJS) => {
|
||||
let currentEnvs = cloneDeep(envs)
|
||||
|
||||
const vm = QuickJS.createVm()
|
||||
|
||||
const pwHandle = vm.newObject()
|
||||
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
const testFuncHandle = vm.newFunction(
|
||||
"test",
|
||||
(descriptorHandle, testFuncHandle) => {
|
||||
const descriptor = vm.getString(descriptorHandle)
|
||||
|
||||
testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
|
||||
const result = vm.unwrapResult(
|
||||
vm.callFunction(testFuncHandle, vm.null)
|
||||
)
|
||||
result.dispose()
|
||||
|
||||
const child = testRunStack.pop() as TestDescriptor
|
||||
testRunStack[testRunStack.length - 1].children.push(child)
|
||||
}
|
||||
)
|
||||
|
||||
const expectFnHandle = vm.newFunction("expect", (expectValueHandle) => {
|
||||
const expectVal = vm.dump(expectValueHandle)
|
||||
|
||||
return {
|
||||
value: createExpectation(vm, expectVal, false, testRunStack),
|
||||
}
|
||||
})
|
||||
|
||||
// Marshal response object
|
||||
const responseObjHandle = marshalObjectToVM(vm, response)
|
||||
if (E.isLeft(responseObjHandle))
|
||||
return TE.left(
|
||||
`Response marshalling failed: ${responseObjHandle.left}`
|
||||
)
|
||||
|
||||
vm.setProp(pwHandle, "response", responseObjHandle.right)
|
||||
responseObjHandle.right.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "expect", expectFnHandle)
|
||||
expectFnHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "test", testFuncHandle)
|
||||
testFuncHandle.dispose()
|
||||
|
||||
// Environment management APIs
|
||||
// TODO: Unified Implementation
|
||||
const envHandle = vm.newObject()
|
||||
|
||||
const envGetHandle = vm.newFunction("get", (keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
O.match(
|
||||
() => vm.undefined,
|
||||
({ value }) => vm.newString(value)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
})
|
||||
|
||||
const envGetResolveHandle = vm.newFunction(
|
||||
"getResolve",
|
||||
(keyHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, currentEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...envs.selected,
|
||||
...envs.global,
|
||||
]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
|
||||
// Create a new VM String
|
||||
// NOTE: Do not shorten this to map(vm.newString) apparently it breaks it
|
||||
E.map((x) => vm.newString(x)),
|
||||
|
||||
E.getOrElse(() => vm.undefined)
|
||||
)
|
||||
|
||||
return {
|
||||
value: result,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const envSetHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
|
||||
const key: unknown = vm.dump(keyHandle)
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected key to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
currentEnvs = setEnv(key, value, currentEnvs)
|
||||
|
||||
return {
|
||||
value: vm.undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const envResolveHandle = vm.newFunction("resolve", (valueHandle) => {
|
||||
const value: unknown = vm.dump(valueHandle)
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return {
|
||||
error: vm.newString("Expected value to be a string"),
|
||||
}
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...currentEnvs.selected,
|
||||
...currentEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return {
|
||||
value: vm.newString(result),
|
||||
}
|
||||
})
|
||||
|
||||
vm.setProp(envHandle, "resolve", envResolveHandle)
|
||||
envResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "set", envSetHandle)
|
||||
envSetHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "getResolve", envGetResolveHandle)
|
||||
envGetResolveHandle.dispose()
|
||||
|
||||
vm.setProp(envHandle, "get", envGetHandle)
|
||||
envGetHandle.dispose()
|
||||
|
||||
vm.setProp(pwHandle, "env", envHandle)
|
||||
envHandle.dispose()
|
||||
|
||||
vm.setProp(vm.global, "pw", pwHandle)
|
||||
pwHandle.dispose()
|
||||
|
||||
const evalRes = vm.evalCode(testScript)
|
||||
|
||||
if (evalRes.error) {
|
||||
const errorData = vm.dump(evalRes.error)
|
||||
evalRes.error.dispose()
|
||||
|
||||
return TE.left(`Script evaluation failed: ${errorData}`)
|
||||
}
|
||||
|
||||
vm.dispose()
|
||||
|
||||
return TE.right({
|
||||
tests: testRunStack,
|
||||
envs: currentEnvs,
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
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 }
|
||||
|
||||
// Run the test script in the provided context
|
||||
runInContext(testScript, context)
|
||||
|
||||
resolve({
|
||||
tests: testRunStack,
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
import { SandboxTestResult, TestResponse, TestResult } from "~/types"
|
||||
|
||||
import Worker from "./worker?worker&inline"
|
||||
|
||||
export const runTestScript = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
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,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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"
|
||||
|
||||
const executeScriptInContext = (
|
||||
testScript: string,
|
||||
envs: TestResult["envs"],
|
||||
response: TestResponse
|
||||
): TE.TaskEither<string, SandboxTestResult> => {
|
||||
try {
|
||||
const responseObjHandle = preventCyclicObjects(response)
|
||||
if (E.isLeft(responseObjHandle)) {
|
||||
return TE.left(`Response marshalling failed: ${responseObjHandle.left}`)
|
||||
}
|
||||
|
||||
const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
|
||||
|
||||
// Create a function from the test script using the `Function` constructor
|
||||
const executeScript = new Function("pw", testScript)
|
||||
|
||||
// Execute the script
|
||||
executeScript({ ...pw, response: responseObjHandle.right })
|
||||
|
||||
return TE.right(<SandboxTestResult>{
|
||||
tests: testRunStack[0],
|
||||
envs: updatedEnvs,
|
||||
})
|
||||
} catch (error) {
|
||||
return TE.left(`Script execution failed: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener("message", async (event) => {
|
||||
const { testScript, envs, response } = event.data
|
||||
|
||||
const results = await executeScriptInContext(testScript, envs, response)()
|
||||
|
||||
// Post the result back to the main thread
|
||||
self.postMessage({ results })
|
||||
})
|
||||
61
packages/hoppscotch-js-sandbox/src/types/index.ts
Normal file
61
packages/hoppscotch-js-sandbox/src/types/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
|
||||
/**
|
||||
* The response object structure exposed to the test script
|
||||
*/
|
||||
export type TestResponse = {
|
||||
/** Status Code of the response */
|
||||
status: number
|
||||
/** List of headers returned */
|
||||
headers: { key: string; value: string }[]
|
||||
/**
|
||||
* Body of the response, this will be the JSON object if it is a JSON content type, else body string
|
||||
*/
|
||||
body: string | object
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of an expectation statement
|
||||
*/
|
||||
export type ExpectResult = {
|
||||
status: "pass" | "fail" | "error"
|
||||
message: string
|
||||
} // The expectation failed (fail) or errored (error)
|
||||
|
||||
/**
|
||||
* An object defining the result of the execution of a
|
||||
* test block
|
||||
*/
|
||||
export type TestDescriptor = {
|
||||
/**
|
||||
* The name of the test block
|
||||
*/
|
||||
descriptor: string
|
||||
|
||||
/**
|
||||
* Expectation results of the test block
|
||||
*/
|
||||
expectResults: ExpectResult[]
|
||||
|
||||
/**
|
||||
* Children test blocks (test blocks inside the test block)
|
||||
*/
|
||||
children: TestDescriptor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the result of a test script execution
|
||||
*/
|
||||
|
||||
export type TestResult = {
|
||||
tests: TestDescriptor[]
|
||||
envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}
|
||||
}
|
||||
|
||||
export type GlobalEnvItem = TestResult["envs"]["global"][number]
|
||||
export type SelectedEnvItem = TestResult["envs"]["selected"][number]
|
||||
|
||||
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
|
||||
@@ -1,89 +1,439 @@
|
||||
import * as O from "fp-ts/Option"
|
||||
import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as QuickJS from "quickjs-emscripten"
|
||||
import { TestResult } from "./test-runner"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
export function marshalObjectToVM(
|
||||
vm: QuickJS.QuickJSVm,
|
||||
obj: object
|
||||
): E.Either<string, QuickJS.QuickJSHandle> {
|
||||
let jsonString
|
||||
import {
|
||||
GlobalEnvItem,
|
||||
SelectedEnvItem,
|
||||
TestDescriptor,
|
||||
TestResult,
|
||||
} from "./types"
|
||||
|
||||
try {
|
||||
jsonString = JSON.stringify(obj)
|
||||
} catch (e) {
|
||||
return E.left("Marshaling stringification failed")
|
||||
}
|
||||
|
||||
const vmStringHandle = vm.newString(jsonString)
|
||||
|
||||
const jsonHandle = vm.getProp(vm.global, "JSON")
|
||||
const parseFuncHandle = vm.getProp(jsonHandle, "parse")
|
||||
|
||||
const parseResultHandle = vm.callFunction(
|
||||
parseFuncHandle,
|
||||
vm.undefined,
|
||||
vmStringHandle
|
||||
)
|
||||
|
||||
if (parseResultHandle.error) {
|
||||
parseResultHandle.error.dispose()
|
||||
return E.left("Marshaling failed")
|
||||
}
|
||||
|
||||
const resultHandle = vm.unwrapResult(parseResultHandle)
|
||||
|
||||
vmStringHandle.dispose()
|
||||
parseFuncHandle.dispose()
|
||||
jsonHandle.dispose()
|
||||
|
||||
return E.right(resultHandle)
|
||||
}
|
||||
|
||||
export function getEnv(envName: string, envs: TestResult["envs"]) {
|
||||
const getEnv = (envName: string, envs: TestResult["envs"]) => {
|
||||
return O.fromNullable(
|
||||
envs.selected.find((x) => x.key === envName) ??
|
||||
envs.global.find((x) => x.key === envName)
|
||||
envs.selected.find((x: SelectedEnvItem) => x.key === envName) ??
|
||||
envs.global.find((x: GlobalEnvItem) => x.key === envName)
|
||||
)
|
||||
}
|
||||
|
||||
export function setEnv(
|
||||
// Compiles shared scripting API methods for use in both pre and post request scripts
|
||||
const getSharedMethods = (envs: TestResult["envs"]) => {
|
||||
let updatedEnvs = envs
|
||||
|
||||
const envGetFn = (key: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, updatedEnvs),
|
||||
O.match(
|
||||
() => undefined,
|
||||
({ value }) => String(value)
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const envGetResolveFn = (key: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
getEnv(key, updatedEnvs),
|
||||
E.fromOption(() => "INVALID_KEY" as const),
|
||||
|
||||
E.map(({ value }) =>
|
||||
pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...updatedEnvs.selected,
|
||||
...updatedEnvs.global,
|
||||
]),
|
||||
// If the recursive resolution failed, return the unresolved value
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
),
|
||||
E.map((x) => String(x)),
|
||||
|
||||
E.getOrElseW(() => undefined)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const envSetFn = (key: any, value: any) => {
|
||||
if (typeof key !== "string") {
|
||||
throw new Error("Expected key to be a string")
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Expected value to be a string")
|
||||
}
|
||||
|
||||
updatedEnvs = setEnv(key, value, updatedEnvs)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const envResolveFn = (value: any) => {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Expected value to be a string")
|
||||
}
|
||||
|
||||
const result = pipe(
|
||||
parseTemplateStringE(value, [
|
||||
...updatedEnvs.selected,
|
||||
...updatedEnvs.global,
|
||||
]),
|
||||
E.getOrElse(() => value)
|
||||
)
|
||||
|
||||
return String(result)
|
||||
}
|
||||
|
||||
return {
|
||||
methods: {
|
||||
env: {
|
||||
get: envGetFn,
|
||||
getResolve: envGetResolveFn,
|
||||
set: envSetFn,
|
||||
resolve: envResolveFn,
|
||||
},
|
||||
},
|
||||
updatedEnvs,
|
||||
}
|
||||
}
|
||||
|
||||
const setEnv = (
|
||||
envName: string,
|
||||
envValue: string,
|
||||
envs: TestResult["envs"]
|
||||
): TestResult["envs"] {
|
||||
const indexInSelected = envs.selected.findIndex((x) => x.key === envName)
|
||||
): TestResult["envs"] => {
|
||||
const { global, selected } = envs
|
||||
|
||||
const indexInSelected = selected.findIndex(
|
||||
(x: SelectedEnvItem) => x.key === envName
|
||||
)
|
||||
|
||||
// Found the match in selected
|
||||
if (indexInSelected >= 0) {
|
||||
envs.selected[indexInSelected].value = envValue
|
||||
selected[indexInSelected].value = envValue
|
||||
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
const indexInGlobal = envs.global.findIndex((x) => x.key == envName)
|
||||
const indexInGlobal = global.findIndex((x: GlobalEnvItem) => x.key == envName)
|
||||
|
||||
// Found a match in globals
|
||||
if (indexInGlobal >= 0) {
|
||||
envs.global[indexInGlobal].value = envValue
|
||||
global[indexInGlobal].value = envValue
|
||||
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't find in both places, create a new variable in selected
|
||||
envs.selected.push({
|
||||
selected.push({
|
||||
key: envName,
|
||||
value: envValue,
|
||||
})
|
||||
|
||||
return {
|
||||
global: envs.global,
|
||||
selected: envs.selected,
|
||||
global,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
export function preventCyclicObjects(
|
||||
obj: Record<string, any>
|
||||
): E.Left<string> | E.Right<Record<string, any>> {
|
||||
let jsonString
|
||||
|
||||
try {
|
||||
jsonString = JSON.stringify(obj)
|
||||
} catch (e) {
|
||||
return E.left("Stringification failed")
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedJson = JSON.parse(jsonString)
|
||||
return E.right(parsedJson)
|
||||
} catch (err) {
|
||||
return E.left("Parsing failed")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Expectation object for use inside the sandbox
|
||||
* @param expectVal The expecting value of the expectation
|
||||
* @param negated Whether the expectation is negated (negative)
|
||||
* @param currTestStack The current state of the test execution stack
|
||||
* @returns Object with the expectation methods
|
||||
*/
|
||||
export const createExpectation = (
|
||||
expectVal: any,
|
||||
negated: boolean,
|
||||
currTestStack: TestDescriptor[]
|
||||
) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
const toBeFn = (expectedVal: any) => {
|
||||
let assertion = expectVal === expectedVal
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be '${expectedVal}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toBeLevelXxx = (
|
||||
level: string,
|
||||
rangeStart: number,
|
||||
rangeEnd: number
|
||||
) => {
|
||||
const parsedExpectVal = parseInt(expectVal)
|
||||
|
||||
if (!Number.isNaN(parsedExpectVal)) {
|
||||
let assertion =
|
||||
parsedExpectVal >= rangeStart && parsedExpectVal <= rangeEnd
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${parsedExpectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be ${level}-level status`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = `Expected ${level}-level status but could not parse value '${expectVal}'`
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toBeLevel2xxFn = () => toBeLevelXxx("200", 200, 299)
|
||||
const toBeLevel3xxFn = () => toBeLevelXxx("300", 300, 399)
|
||||
const toBeLevel4xxFn = () => toBeLevelXxx("400", 400, 499)
|
||||
const toBeLevel5xxFn = () => toBeLevelXxx("500", 500, 599)
|
||||
|
||||
const toBeTypeFn = (expectedType: any) => {
|
||||
if (
|
||||
[
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
let assertion = typeof expectVal === expectedType
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected '${expectVal}' to${
|
||||
negated ? " not" : ""
|
||||
} be type '${expectedType}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message =
|
||||
'Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"'
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toHaveLengthFn = (expectedLength: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
const message =
|
||||
"Expected toHaveLength to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) {
|
||||
let assertion = expectVal.length === expectedLength
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected the array to${
|
||||
negated ? " not" : ""
|
||||
} be of length '${expectedLength}'`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
const message = "Argument for toHaveLength should be a number"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const toIncludeFn = (needle: any) => {
|
||||
if (!(Array.isArray(expectVal) || typeof expectVal === "string")) {
|
||||
const message = "Expected toInclude to be called for an array or string"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (needle === null) {
|
||||
const message = "Argument for toInclude should not be null"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (needle === undefined) {
|
||||
const message = "Argument for toInclude should not be undefined"
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status: "error",
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
let assertion = expectVal.includes(needle)
|
||||
|
||||
if (negated) {
|
||||
assertion = !assertion
|
||||
}
|
||||
|
||||
const expectValPretty = JSON.stringify(expectVal)
|
||||
const needlePretty = JSON.stringify(needle)
|
||||
const status = assertion ? "pass" : "fail"
|
||||
const message = `Expected ${expectValPretty} to${
|
||||
negated ? " not" : ""
|
||||
} include ${needlePretty}`
|
||||
|
||||
currTestStack[currTestStack.length - 1].expectResults.push({
|
||||
status,
|
||||
message,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
result.toBe = toBeFn
|
||||
result.toBeLevel2xx = toBeLevel2xxFn
|
||||
result.toBeLevel3xx = toBeLevel3xxFn
|
||||
result.toBeLevel4xx = toBeLevel4xxFn
|
||||
result.toBeLevel5xx = toBeLevel5xxFn
|
||||
result.toBeType = toBeTypeFn
|
||||
result.toHaveLength = toHaveLengthFn
|
||||
result.toInclude = toIncludeFn
|
||||
|
||||
Object.defineProperties(result, {
|
||||
not: {
|
||||
get: () => createExpectation(expectVal, !negated, currTestStack),
|
||||
},
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles methods for use under the `pw` namespace for pre request scripts
|
||||
* @param envs The current state of the environment variables
|
||||
* @returns Object with methods in the `pw` namespace
|
||||
*/
|
||||
export const getPreRequestScriptMethods = (envs: TestResult["envs"]) => {
|
||||
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
|
||||
return { pw: methods, updatedEnvs }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles methods for use under the `pw` namespace for post request scripts
|
||||
* @param envs The current state of the environment variables
|
||||
* @returns Object with methods in the `pw` namespace and test run stack
|
||||
*/
|
||||
export const getTestRunnerScriptMethods = (envs: TestResult["envs"]) => {
|
||||
const testRunStack: TestDescriptor[] = [
|
||||
{ descriptor: "root", expectResults: [], children: [] },
|
||||
]
|
||||
|
||||
const testFn = (descriptor: string, testFunc: () => void) => {
|
||||
testRunStack.push({
|
||||
descriptor,
|
||||
expectResults: [],
|
||||
children: [],
|
||||
})
|
||||
|
||||
testFunc()
|
||||
|
||||
const child = testRunStack.pop() as TestDescriptor
|
||||
testRunStack[testRunStack.length - 1].children.push(child)
|
||||
}
|
||||
|
||||
const expectFn = (expectVal: any) =>
|
||||
createExpectation(expectVal, false, testRunStack)
|
||||
|
||||
const { methods, updatedEnvs } = getSharedMethods(cloneDeep(envs))
|
||||
|
||||
const pw = {
|
||||
...methods,
|
||||
expect: expectFn,
|
||||
test: testFn,
|
||||
}
|
||||
|
||||
return { pw, testRunStack, updatedEnvs }
|
||||
}
|
||||
|
||||
2
packages/hoppscotch-js-sandbox/src/web.ts
Normal file
2
packages/hoppscotch-js-sandbox/src/web.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./pre-request/web-worker"
|
||||
export * from "./test-runner/web-worker"
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"types": ["@types/node", "@types/jest", "@relmify/jest-fp-ts"],
|
||||
"outDir": "./lib/",
|
||||
"rootDir": "./src/",
|
||||
"types": ["@types/node", "@types/jest", "@relmify/jest-fp-ts", "vite/client"],
|
||||
"outDir": "./dist/",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
outDir: "./lib/",
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
splitting: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
})
|
||||
25
packages/hoppscotch-js-sandbox/vite.config.ts
Normal file
25
packages/hoppscotch-js-sandbox/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { resolve } from "path"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: {
|
||||
web: "./src/web.ts",
|
||||
node: "./src/node.ts",
|
||||
},
|
||||
name: "js-sandbox",
|
||||
formats: ["es", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vm"],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
2
packages/hoppscotch-js-sandbox/web.d.ts
vendored
Normal file
2
packages/hoppscotch-js-sandbox/web.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/web.d.ts"
|
||||
export * from "./dist/web.d.ts"
|
||||
Reference in New Issue
Block a user