Merge pull request #1836 from AndrewBastin/refactor/js-sandbox

Move to JS code execution sandbox (HOPP-67, HOPP-66)
This commit is contained in:
Andrew Bastin
2021-09-25 22:27:42 +05:30
32 changed files with 2094 additions and 510 deletions

View File

@@ -302,9 +302,10 @@ _Sample keys only works with the [production build](https://hoppscotch.io)._
### Local development environment
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
3. Start the development server with `pnpm run dev`.
4. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
2. Install pnpm using npm by running `npm install -g pnpm`.
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
4. Start the development server with `pnpm run dev`.
5. Open development site by going to [`http://localhost:3000`](http://localhost:3000) in your browser.
### Docker compose
@@ -323,9 +324,10 @@ docker run --rm --name hoppscotch -p 3000:3000 hoppscotch/hoppscotch:latest
## **Releasing**
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
2. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
3. Build the release files with `pnpm run generate`.
4. Find the built project in `packages/hoppscotch-app/dist`.
2. Install pnpm using npm by running `npm install -g pnpm`.
3. Install dependencies by running `pnpm install` within the directory that you cloned (probably `hoppscotch`).
4. Build the release files with `pnpm run generate`.
5. Find the built project in `packages/hoppscotch-app/dist`.
## **Contributing**

View File

@@ -11,7 +11,8 @@
"generate": "pnpm -r do-build-prod",
"start": "pnpm -r do-prod-start",
"lintfix": "pnpm -r do-lintfix",
"pre-commit": "pnpm -r do-lintfix"
"pre-commit": "pnpm -r do-lintfix",
"test": "pnpm -r do-test"
},
"workspaces": [
"./packages/*"

View File

@@ -232,6 +232,7 @@
<script setup lang="ts">
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
import { isRight } from "fp-ts/lib/Either"
import {
updateRESTResponse,
restEndpoint$,
@@ -303,25 +304,31 @@ watch(loading, () => {
}
})
const newSendRequest = () => {
const newSendRequest = async () => {
loading.value = true
subscribeToStream(
runRESTRequest$(),
(responseState) => {
if (loading.value) {
// Check exists because, loading can be set to false
// when cancelled
updateRESTResponse(responseState)
// Double calling is because the function returns a TaskEither than should be executed
const streamResult = await runRESTRequest$()()
// TODO: What if stream fetching failed (script execution errors ?) (isLeft)
if (isRight(streamResult)) {
subscribeToStream(
streamResult.right,
(responseState) => {
if (loading.value) {
// Check exists because, loading can be set to false
// when cancelled
updateRESTResponse(responseState)
}
},
() => {
loading.value = false
},
() => {
loading.value = false
}
},
() => {
loading.value = false
},
() => {
loading.value = false
}
)
)
}
}
const cancelRequest = () => {

View File

@@ -1,155 +1,86 @@
import { Observable } from "rxjs"
import { filter } from "rxjs/operators"
import getEnvironmentVariablesFromScript from "./preRequest"
import { chain, right, TaskEither } from "fp-ts/lib/TaskEither"
import { pipe } from "fp-ts/lib/function"
import { runTestScript, TestDescriptor } from "@hoppscotch/js-sandbox"
import { isRight } from "fp-ts/lib/Either"
import {
getCombinedEnvVariables,
getFinalEnvsFromPreRequest,
} from "./preRequest"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { createRESTNetworkRequestStream } from "./network"
import runTestScriptWithVariables, {
transformResponseForTesting,
} from "./postwomanTesting"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { isJSONContentType } from "./utils/contenttypes"
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
/**
* Runs a REST network request along with all the
* other side processes (like running test scripts)
*/
export function runRESTRequest$(): Observable<HoppRESTResponse> {
const envs = getEnvironmentVariablesFromScript(
getRESTRequest().preRequestScript
const getTestableBody = (res: HoppRESTResponse & { type: "success" }) => {
const contentTypeHeader = res.headers.find(
(h) => h.value.toLowerCase() === "content-type"
)
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
name: "Env",
variables: Object.keys(envs).map((key) => {
return {
key,
value: envs[key],
}
}),
})
const rawBody = new TextDecoder("utf-8").decode(res.body)
const stream = createRESTNetworkRequestStream(effectiveRequest)
if (!contentTypeHeader || !isJSONContentType(contentTypeHeader.value))
return rawBody
// Run Test Script when request ran successfully
const subscription = stream
.pipe(filter((res) => res.type === "success"))
.subscribe((res) => {
const testReport: {
report: "" // ¯\_(ツ)_/¯
testResults: Array<
| {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
return JSON.parse(rawBody)
}
export const runRESTRequest$ = (): TaskEither<
string,
Observable<HoppRESTResponse>
> =>
pipe(
getFinalEnvsFromPreRequest(
getRESTRequest().preRequestScript,
getCombinedEnvVariables()
),
chain((envs) => {
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
name: "Env",
variables: envs,
})
const stream = createRESTNetworkRequestStream(effectiveRequest)
// Run Test Script when request ran successfully
const subscription = stream
.pipe(filter((res) => res.type === "success"))
.subscribe(async (res) => {
if (res.type === "success") {
const runResult = await runTestScript(res.req.testScript, {
status: res.statusCode,
body: getTestableBody(res),
headers: res.headers,
})()
// TODO: Handle script executation fails (isLeft)
if (isRight(runResult)) {
setRESTTestResults(translateToSandboxTestResults(runResult.right))
}
| {
result: "PASS"
message: string
styles: { icon: "check"; class: "success-response" }
}
| { startBlock: string; styles: { icon: ""; class: "" } }
| { endBlock: true; styles: { icon: ""; class: "" } }
>
errors: [] // ¯\_(ツ)_/¯
} = runTestScriptWithVariables(effectiveRequest.testScript, {
response: transformResponseForTesting(res),
}) as any
setRESTTestResults(translateToNewTestResults(testReport))
subscription.unsubscribe()
}
})
subscription.unsubscribe()
return right(stream)
})
)
return stream
}
function isTestPass(x: any): x is {
result: "PASS"
styles: { icon: "check"; class: "success-response" }
} {
return x.result !== undefined && x.result === "PASS"
}
function isTestFail(x: any): x is {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
} {
return x.result !== undefined && x.result === "FAIL"
}
function isStartBlock(
x: any
): x is { startBlock: string; styles: { icon: ""; class: "" } } {
return x.startBlock !== undefined
}
function isEndBlock(
x: any
): x is { endBlock: true; styles: { icon: ""; class: "" } } {
return x.endBlock !== undefined
}
function translateToNewTestResults(testReport: {
report: "" // ¯\_(ツ)_/¯
testResults: Array<
| {
result: "FAIL"
message: string
styles: { icon: "close"; class: "cl-error-response" }
}
| {
result: "PASS"
message: string
styles: { icon: "check"; class: "success-response" }
}
| { startBlock: string; styles: { icon: ""; class: "" } }
| { endBlock: true; styles: { icon: ""; class: "" } }
>
errors: [] // ¯\_(ツ)_/¯
}): HoppTestResult {
// Build a stack of test data which we eventually build up based on the results
const testsStack: HoppTestData[] = [
{
description: "root",
tests: [],
expectResults: [],
},
]
testReport.testResults.forEach((result) => {
// This is a test block start, push an empty test to the stack
if (isStartBlock(result)) {
testsStack.push({
description: result.startBlock,
tests: [],
expectResults: [],
})
} else if (isEndBlock(result)) {
// End of the block, pop the stack and add it as a child to the current stack top
const testData = testsStack.pop()!
testsStack[testsStack.length - 1].tests.push(testData)
} else if (isTestPass(result)) {
// A normal PASS expectation
testsStack[testsStack.length - 1].expectResults.push({
status: "pass",
message: result.message,
})
} else if (isTestFail(result)) {
// A normal FAIL expectation
testsStack[testsStack.length - 1].expectResults.push({
status: "fail",
message: result.message,
})
function translateToSandboxTestResults(
testDesc: TestDescriptor
): HoppTestResult {
const translateChildTests = (child: TestDescriptor): HoppTestData => {
return {
description: child.descriptor,
expectResults: child.expectResults,
tests: child.children.map(translateChildTests),
}
})
// We should end up with only the root stack entry
if (testsStack.length !== 1) throw new Error("Invalid test result structure")
}
return {
expectResults: testsStack[0].expectResults,
tests: testsStack[0].tests,
expectResults: testDesc.expectResults,
tests: testDesc.children.map(translateChildTests),
}
}

View File

@@ -1,321 +0,0 @@
import { HoppRESTResponse } from "./types/HoppRESTResponse"
const styles = {
PASS: { icon: "check", class: "success-response" },
FAIL: { icon: "close", class: "cl-error-response" },
ERROR: { icon: "close", class: "cl-error-response" },
}
type TestScriptResponse = {
body: any
headers: any[]
status: number
__newRes: HoppRESTResponse
}
type TestScriptVariables = {
response: TestScriptResponse
}
type TestReportStartBlock = {
startBlock: string
}
type TestReportEndBlock = {
endBlock: true
}
type TestReportEntry = {
result: "PASS" | "FAIL" | "ERROR"
message: string
styles: {
icon: string
}
}
type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
export default function runTestScriptWithVariables(
script: string,
variables?: TestScriptVariables
) {
const pw = {
_errors: [],
_testReports: [] as TestReport[],
_report: "",
expect(value: any) {
try {
return expect(value, this._testReports)
} catch (e: any) {
pw._testReports.push({
result: "ERROR",
message: `${e}`,
styles: styles.ERROR,
})
}
},
test: (descriptor: string, func: () => void) =>
test(descriptor, func, pw._testReports),
// globals that the script is allowed to have access to.
}
Object.assign(pw, variables)
// run pre-request script within this function so that it has access to the pw object.
// eslint-disable-next-line no-new-func
new Function("pw", script)(pw)
return {
report: pw._report,
errors: pw._errors,
testResults: pw._testReports,
}
}
function test(
descriptor: string,
func: () => void,
_testReports: TestReport[]
) {
_testReports.push({ startBlock: descriptor })
try {
func()
} catch (e: any) {
_testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
}
_testReports.push({ endBlock: true })
// TODO: Organize and generate text report of each {descriptor: true} section in testReports.
// add checkmark or x depending on if each testReport is pass=true or pass=false
}
function expect(expectValue: any, _testReports: TestReport[]) {
return new Expectation(expectValue, null, _testReports)
}
class Expectation {
private expectValue: any
private not: true | Expectation
private _testReports: TestReport[]
constructor(
expectValue: any,
_not: boolean | null,
_testReports: TestReport[]
) {
this.expectValue = expectValue
this.not = _not || new Expectation(this.expectValue, true, _testReports)
this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
}
private _satisfies(expectValue: any, targetValue?: any): boolean {
// Used for testing if two values match the expectation, which could be === OR !==, depending on if not
// was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
// Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
if (!targetValue) {
targetValue = expectValue
expectValue = this.expectValue
}
if (this.not === true) {
// test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
return expectValue !== targetValue
} else {
return expectValue === targetValue
}
}
_fmtNot(message: string) {
// given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
if (this.not === true) {
return message.replace("(not)", "not ")
} else {
return message.replace("(not)", "")
}
}
_fail(message: string) {
return this._testReports.push({
result: "FAIL",
message,
styles: styles.FAIL,
})
}
_pass(message: string) {
return this._testReports.push({
result: "PASS",
message,
styles: styles.PASS,
})
}
// TEST METHODS DEFINED BELOW
// these are the usual methods that would follow expect(...)
toBe(value: any) {
return this._satisfies(value)
? this._pass(
this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
)
: this._fail(
this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
)
}
toHaveProperty(value: string) {
return this._satisfies(
Object.prototype.hasOwnProperty.call(this.expectValue, value),
true
)
? this._pass(
this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
)
: this._fail(
this._fmtNot(
`Expected object ${this.expectValue} to (not)have property ${value}`
)
)
}
toBeLevel2xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 200-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 200 && code < 300, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 200-level status`
)
)
}
toBeLevel3xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 300-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 300 && code < 400, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 300-level status`
)
)
}
toBeLevel4xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 400-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 400 && code < 500, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 400-level status`
)
)
}
toBeLevel5xx() {
const code = parseInt(this.expectValue, 10)
if (Number.isNaN(code)) {
return this._fail(
`Expected 500-level status but could not parse value ${this.expectValue}`
)
}
return this._satisfies(code >= 500 && code < 600, true)
? this._pass(
this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
)
: this._fail(
this._fmtNot(
`Expected ${this.expectValue} to (not)be 500-level status`
)
)
}
toHaveLength(expectedLength: number) {
const actualLength = this.expectValue.length
return this._satisfies(actualLength, expectedLength)
? this._pass(
this._fmtNot(
`Length expectation of (not)being ${expectedLength} is kept`
)
)
: this._fail(
this._fmtNot(
`Expected length to be ${expectedLength} but actual length was ${actualLength}`
)
)
}
toBeType(expectedType: string) {
const actualType = typeof this.expectValue
if (
![
"string",
"boolean",
"number",
"object",
"undefined",
"bigint",
"symbol",
"function",
].includes(expectedType)
) {
return this._fail(
this._fmtNot(
`Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
)
)
}
return this._satisfies(actualType, expectedType)
? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
: this._fail(
this._fmtNot(
`Expected type to be "${expectedType}" but actual type was "${actualType}"`
)
)
}
}
export function transformResponseForTesting(
response: HoppRESTResponse
): TestScriptResponse {
if (response.type === "loading") {
throw new Error("Cannot transform loading responses")
}
if (response.type === "network_fail") {
throw new Error("Cannot transform failed responses")
}
let body: any = new TextDecoder("utf-8").decode(response.body)
// Try parsing to JSON
try {
body = JSON.parse(body)
} catch (_) {}
return {
body,
headers: response.headers,
status: response.statusCode,
__newRes: response,
}
}

View File

@@ -1,42 +1,29 @@
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
import {
getCurrentEnvironment,
getGlobalVariables,
} from "~/newstore/environments"
export default function getEnvironmentVariablesFromScript(script: string) {
const _variables: Record<string, string> = {}
export const getCombinedEnvVariables = () => {
const variables: { key: string; value: string }[] = [...getGlobalVariables()]
const currentEnv = getCurrentEnvironment()
for (const variable of getCurrentEnvironment().variables) {
const index = variables.findIndex((v) => variable.key === v.key)
for (const variable of currentEnv.variables) {
_variables[variable.key] = variable.value
}
const globalEnv = getGlobalVariables()
if (globalEnv) {
for (const variable of globalEnv) {
_variables[variable.key] = variable.value
if (index === -1) {
variables.push({
key: variable.key,
value: variable.value,
})
} else {
variables[index].value = variable.value
}
}
try {
// the pw object is the proxy by which pre-request scripts can pass variables to the request.
// for security and control purposes, this is the only way a pre-request script should modify variables.
const pw = {
environment: {
set: (key: string, value: string) => (_variables[key] = value),
},
env: {
set: (key: string, value: string) => (_variables[key] = value),
},
// globals that the script is allowed to have access to.
}
// run pre-request script within this function so that it has access to the pw object.
// eslint-disable-next-line no-new-func
new Function("pw", script)(pw)
} catch (_e) {}
return _variables
return variables
}
export const getFinalEnvsFromPreRequest = (
script: string,
envs: { key: string; value: string }[]
) => runPreRequestScript(script, envs)

View File

@@ -22,10 +22,12 @@
"do-dev": "pnpm run dev",
"do-build-prod": "pnpm run generate",
"do-prod-start": "pnpm run start",
"do-lintfix": "pnpm run lint"
"do-lintfix": "pnpm run lint",
"do-test": "pnpm run test"
},
"dependencies": {
"@apollo/client": "^3.4.12",
"@hoppscotch/js-sandbox": "workspace:^1.0.0",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.29.0",
"@nuxtjs/gtm": "^2.4.0",
@@ -41,6 +43,7 @@
"core-js": "^3.17.3",
"esprima": "^4.0.1",
"firebase": "^9.0.2",
"fp-ts": "^2.11.3",
"fuse.js": "^6.4.6",
"graphql": "^15.5.3",
"graphql-language-service-interface": "^2.8.4",

View File

@@ -0,0 +1,29 @@
module.exports = {
root: true,
env: {
node: true,
jest: true,
browser: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaVersion: 2021,
},
plugins: ["prettier"],
extends: [
"prettier/prettier",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
semi: [2, "never"],
"prettier/prettier": ["warn", { semi: false }],
"import/no-named-as-default": "off",
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "off",
},
}

View File

@@ -0,0 +1,5 @@
node_modules
coverage
.husky/
dist
lib

View File

@@ -0,0 +1,8 @@
.dependabot
.github
.hoppscotch
.vscode
package-lock.json
node_modules
dist
lib

View File

@@ -0,0 +1,3 @@
module.exports = {
semi: false,
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,65 @@
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
</a>
</div>
<div align="center">
# Hoppscotch JavaScript Sandbox <font size=2><sup>ALPHA</sup></font>
</div>
This package deals with providing a JavaScript sandbox for executing various security sensitive external scripts.
## Usage
Install the [npm package](https://www.npmjs.com/package/@hoppscotch/js-sandbox).
```
npm install --save @hoppscotch/js-sandbox
```
## How does this work?
This package makes use of [quickjs-empscripten](https://www.npmjs.com/package/quickjs-emscripten) for building sandboxes for running external code on Hoppscotch.
Currently implemented sandboxes:
- Hoppscotch Test Scripts
## Development
1. Clone the repository
```
git clone https://github.com/hoppscotch/hopp-js-sandbox
```
2. Install the package dependencies
```
npm install
```
3. Try out the demo [`src/demo.ts`](https://github.com/hoppscotch/hopp-js-sandbox/blob/main/src/demo.ts) using:
```
npm run demo
```
## Versioning
This project follows [Semantic Versioning](https://semver.org/) but as the project is still pre-1.0. The code and the public exposed API should not be considered to be fixed and stable. Things can change at any time!
## License
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see [`LICENSE`](https://github.com/hoppscotch/hopp-js-sandbox/blob/main/LICENSE) for more details.
<div align="center">
<br />
<br />
###### built with ❤︎ by the [Hoppscotch Team](https://github.com/hoppscotch) and [contributors](https://github.com/AndrewBastin/hopp-js-sandbox/graphs/contributors).
</div>

View File

@@ -0,0 +1,6 @@
export default {
preset: "ts-jest",
testEnvironment: "node",
collectCoverage: true,
setupFilesAfterEnv: ['./jest.setup.ts'],
}

View File

@@ -0,0 +1 @@
require('@relmify/jest-fp-ts');

View File

@@ -0,0 +1,60 @@
{
"name": "@hoppscotch/js-sandbox",
"version": "1.0.0",
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
"main": "./lib/index.js",
"types": "./lib/",
"type": "module",
"engines": {
"node": ">=14",
"pnpm": ">=3"
},
"scripts": {
"demo": "esrun src/demo.ts",
"lint": "eslint --ext .ts,.js --ignore-path .gitignore",
"test": "npx jest",
"build": "npx tsc",
"clean": "npx tsc --build --clean",
"prepublish": "pnpm run build",
"do-lintfix": "pnpm run lint",
"do-build-prod": "pnpm run build",
"do-test": "pnpm run test"
},
"keywords": [
"hoppscotch",
"sandbox",
"js-sandbox",
"apis",
"test-runner"
],
"author": "The Hoppscotch Team <support@hoppscotch.io> (https://hoppscotch.com/)",
"license": "MIT",
"dependencies": {
"fp-ts": "^2.11.3",
"lodash": "^4.17.21",
"quickjs-emscripten": "^0.13.0"
},
"devDependencies": {
"@digitak/esrun": "^1.2.4",
"@relmify/jest-fp-ts": "^1.1.1",
"@types/jest": "^26.0.23",
"@types/lodash": "^4.14.173",
"@types/node": "^15.12.5",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"io-ts": "^2.2.16",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"pretty-quick": "^3.1.1",
"ts-jest": "^27.0.3",
"typescript": "^4.3.5"
},
"jest": {
"setupFilesAfterEnv": [
"@relmify/jest-fp-ts"
]
}
}

View File

@@ -0,0 +1,64 @@
import { execPreRequestScript } from "../preRequest"
import "@relmify/jest-fp-ts"
describe("execPreRequestScript", () => {
test("returns the updated envirionment properly", () => {
return expect(
execPreRequestScript(
`
pw.env.set("bob", "newbob")
`,
[{ key: "bob", value: "oldbob" }, { key: "foo", value: "bar" }]
)()
).resolves.toEqualRight([
{ key: "bob", value: "newbob" },
{ key: "foo", value: "bar" }
])
})
test("fails if the key is not a string", () => {
return expect(
execPreRequestScript(
`
pw.env.set(10, "newbob")
`,
[{ key: "bob", value: "oldbob" }, { key: "foo", value: "bar" }]
)()
).resolves.toBeLeft()
})
test("fails if the value is not a string", () => {
return expect(
execPreRequestScript(
`
pw.env.set("bob", 10)
`,
[{ key: "bob", value: "oldbob" }, { key: "foo", value: "bar" }]
)()
).resolves.toBeLeft()
})
test("fails for invalid syntax", () => {
return expect(
execPreRequestScript(
`
pw.env.set("bob",
`,
[{ key: "bob", value: "oldbob" }, { key: "foo", value: "bar" }]
)()
).resolves.toBeLeft()
})
test("creates new env variable if doesn't exist", () => {
return expect(
execPreRequestScript(
`
pw.env.set("foo", "bar")
`,
[]
)()
).resolves.toEqualRight(
[{ key: "foo", value: "bar" }]
)
})
})

View File

@@ -0,0 +1,98 @@
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
}
describe("toBe", () => {
describe("general assertion (no negation)", () => {
test("expect equals expected passes assertion", () => {
return expect(
execTestScript(
`
pw.expect(2).toBe(2)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "pass", message: "Expected '2' to be '2'" }],
}),
])
})
test("expect not equals expected fails assertion", () => {
return expect(
execTestScript(
`
pw.expect(2).toBe(4)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{ status: "fail", message: "Expected '2' to be '4'" }],
}),
])
})
})
describe("general assertion (with negation)", () => {
test("expect equals expected fails assertion", () => {
return expect(
execTestScript(
`
pw.expect(2).not.toBe(2)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: "Expected '2' to not be '2'",
}],
}),
])
})
test("expect not equals expected passes assertion", () => {
return expect(
execTestScript(
`
pw.expect(2).not.toBe(4)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: "Expected '2' to not be '4'",
}],
}),
])
})
})
})
test("strict checks types", () => {
return expect(
execTestScript(
`
pw.expect(2).toBe("2")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: "Expected '2' to be '2'",
}],
}),
])
})

View File

@@ -0,0 +1,361 @@
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
}
describe("toBeLevel2xx", () => {
test("assertion passes for 200 series with no negation", async () => {
for (let i = 200; i < 300; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to be 200-level status`,
}],
}),
])
}
})
test("assertion fails for non 200 series with no negation", async () => {
for (let i = 300; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to be 200-level status`,
}],
}),
])
}
})
test("give error if the expect value was not a number with no negation", async () => {
await expect(
execTestScript(`pw.expect("foo").toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 200-level status but could not parse value 'foo'",
}],
})
])
})
test("assertion fails for 200 series with negation", async () => {
for (let i = 200; i < 300; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to not be 200-level status`,
}],
}),
])
}
})
test("assertion passes for non 200 series with negation", async () => {
for (let i = 300; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to not be 200-level status`,
}],
}),
])
}
})
test("give error if the expect value was not a number with negation", async () => {
await expect(
execTestScript(`pw.expect("foo").not.toBeLevel2xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 200-level status but could not parse value 'foo'",
}],
})
])
})
})
describe("toBeLevel3xx", () => {
test("assertion passes for 300 series with no negation", async () => {
for (let i = 300; i < 400; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to be 300-level status`,
}],
}),
])
}
})
test("assertion fails for non 300 series with no negation", async () => {
for (let i = 400; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to be 300-level status`,
}],
}),
])
}
})
test("give error if the expect value is not a number without negation", () => {
return expect(
execTestScript(`pw.expect("foo").toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 300-level status but could not parse value 'foo'",
}],
})
])
})
test("assertion fails for 400 series with negation", async () => {
for (let i = 300; i < 400; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to not be 300-level status`,
}],
}),
])
}
})
test("assertion passes for non 200 series with negation", async () => {
for (let i = 400; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to not be 300-level status`,
}],
}),
])
}
})
test("give error if the expect value is not a number with negation", () => {
return expect(
execTestScript(`pw.expect("foo").not.toBeLevel3xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 300-level status but could not parse value 'foo'",
}]
})
])
})
})
describe("toBeLevel4xx", () => {
test("assertion passes for 400 series with no negation", async () => {
for (let i = 400; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to be 400-level status`,
}],
}),
])
}
})
test("assertion fails for non 400 series with no negation", async () => {
for (let i = 500; i < 600; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to be 400-level status`,
}],
}),
])
}
})
test("give error if the expected value is not a number without negation", () => {
return expect(
execTestScript(`pw.expect("foo").toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 400-level status but could not parse value 'foo'",
}],
})
])
})
test("assertion fails for 400 series with negation", async () => {
for (let i = 400; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to not be 400-level status`,
}],
}),
])
}
})
test("assertion passes for non 400 series with negation", async () => {
for (let i = 500; i < 600; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to not be 400-level status`,
}],
}),
])
}
})
test("give error if the expected value is not a number with negation", () => {
return expect(
execTestScript(`pw.expect("foo").not.toBeLevel4xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 400-level status but could not parse value 'foo'",
}],
})
])
})
})
describe("toBeLevel5xx", () => {
test("assertion passes for 500 series with no negation", async () => {
for (let i = 500; i < 600; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to be 500-level status`,
}],
}),
])
}
})
test("assertion fails for non 500 series with no negation", async () => {
for (let i = 200; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to be 500-level status`,
}],
}),
])
}
})
test("give error if the expect value is not a number with no negation", () => {
return expect(
execTestScript(`pw.expect("foo").toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 500-level status but could not parse value 'foo'",
}],
})
])
})
test("assertion fails for 500 series with negation", async () => {
for (let i = 500; i < 600; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "fail",
message: `Expected '${i}' to not be 500-level status`,
}],
}),
])
}
})
test("assertion passes for non 500 series with negation", async () => {
for (let i = 200; i < 500; i++) {
await expect(
execTestScript(`pw.expect(${i}).not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "pass",
message: `Expected '${i}' to not be 500-level status`,
}],
}),
])
}
})
test("give error if the expect value is not a number with negation", () => {
return expect(
execTestScript(`pw.expect("foo").not.toBeLevel5xx()`, fakeResponse)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [{
status: "error",
message: "Expected 500-level status but could not parse value 'foo'",
}],
})
])
})
})

View File

@@ -0,0 +1,157 @@
import { execTestScript, TestResponse } from "../../../test-runner"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
}
describe("toBeType", () => {
test("asserts true for valid type expectations with no negation", () => {
return expect(
execTestScript(
`
pw.expect(2).toBeType("number")
pw.expect("2").toBeType("string")
pw.expect(true).toBeType("boolean")
pw.expect({}).toBeType("object")
pw.expect(undefined).toBeType("undefined")
`, fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: `Expected '2' to be type 'number'` },
{ status: "pass", message: `Expected '2' to be type 'string'` },
{ status: "pass", message: `Expected 'true' to be type 'boolean'` },
{ status: "pass", message: `Expected '[object Object]' to be type 'object'` },
{ status: "pass", message: `Expected 'undefined' to be type 'undefined'` },
],
}),
])
})
test("asserts false for invalid type expectations with no negation", () => {
return expect(
execTestScript(
`
pw.expect(2).toBeType("string")
pw.expect("2").toBeType("number")
pw.expect(true).toBeType("string")
pw.expect({}).toBeType("number")
pw.expect(undefined).toBeType("number")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "fail", message: `Expected '2' to be type 'string'`},
{ status: "fail", message: `Expected '2' to be type 'number'`},
{ status: "fail", message: `Expected 'true' to be type 'string'`},
{ status: "fail", message: `Expected '[object Object]' to be type 'number'`},
{ status: "fail", message: `Expected 'undefined' to be type 'number'`},
],
}),
])
})
test("asserts false for valid type expectations with negation", () => {
return expect(
execTestScript(
`
pw.expect(2).not.toBeType("number")
pw.expect("2").not.toBeType("string")
pw.expect(true).not.toBeType("boolean")
pw.expect({}).not.toBeType("object")
pw.expect(undefined).not.toBeType("undefined")
`, fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "fail", message: `Expected '2' to not be type 'number'` },
{ status: "fail", message: `Expected '2' to not be type 'string'` },
{ status: "fail", message: `Expected 'true' to not be type 'boolean'` },
{ status: "fail", message: `Expected '[object Object]' to not be type 'object'` },
{ status: "fail", message: `Expected 'undefined' to not be type 'undefined'` },
],
}),
])
})
test("asserts true for invalid type expectations with negation", () => {
return expect(
execTestScript(
`
pw.expect(2).not.toBeType("string")
pw.expect("2").not.toBeType("number")
pw.expect(true).not.toBeType("string")
pw.expect({}).not.toBeType("number")
pw.expect(undefined).not.toBeType("number")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: `Expected '2' to not be type 'string'` },
{ status: "pass", message: `Expected '2' to not be type 'number'` },
{ status: "pass", message: `Expected 'true' to not be type 'string'` },
{ status: "pass", message: `Expected '[object Object]' to not be type 'number'` },
{ status: "pass", message: `Expected 'undefined' to not be type 'number'` },
],
}),
])
})
test("gives error for invalid type names without negation", () => {
return expect(
execTestScript(
`
pw.expect(2).toBeType("foo")
pw.expect("2").toBeType("bar")
pw.expect(true).toBeType("baz")
pw.expect({}).toBeType("qux")
pw.expect(undefined).toBeType("quux")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
]
})
])
})
test("gives error for invalid type names with negation", () => {
return expect(
execTestScript(
`
pw.expect(2).not.toBeType("foo")
pw.expect("2").not.toBeType("bar")
pw.expect(true).not.toBeType("baz")
pw.expect({}).not.toBeType("qux")
pw.expect(undefined).not.toBeType("quux")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
{ status: "error", message: `Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"` },
]
})
])
})
})

View File

@@ -0,0 +1,157 @@
import { execTestScript, TestResponse } from "../../../test-runner"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
}
describe("toHaveLength", () => {
test("asserts true for valid lengths with no negation", () => {
return expect(
execTestScript(
`
pw.expect([1, 2, 3, 4]).toHaveLength(4)
pw.expect([]).toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected the array to be of length '4'" },
{ status: "pass", message: "Expected the array to be of length '0'" },
],
}),
])
})
test("asserts false for invalid lengths with no negation", () => {
return expect(
execTestScript(
`
pw.expect([]).toHaveLength(4)
pw.expect([1, 2, 3, 4]).toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "fail", message: "Expected the array to be of length '4'" },
{ status: "fail", message: "Expected the array to be of length '0'" },
],
}),
])
})
test("asserts false for valid lengths with negation", () => {
return expect(
execTestScript(
`
pw.expect([1, 2, 3, 4]).not.toHaveLength(4)
pw.expect([]).not.toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "fail", message: "Expected the array to not be of length '4'" },
{ status: "fail", message: "Expected the array to not be of length '0'" },
],
}),
])
})
test("asserts true for invalid lengths with negation", () => {
return expect(
execTestScript(
`
pw.expect([]).not.toHaveLength(4)
pw.expect([1, 2, 3, 4]).not.toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "pass", message: "Expected the array to not be of length '4'" },
{ status: "pass", message: "Expected the array to not be of length '0'" },
],
}),
])
})
test("gives error if not called on an array or a string with no negation", () => {
return expect(
execTestScript(
`
pw.expect(5).toHaveLength(0)
pw.expect(true).toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: "Expected toHaveLength to be called for an array or string" },
{ status: "error", message: "Expected toHaveLength to be called for an array or string" },
]
})
])
})
test("gives error if not called on an array or a string with negation", () => {
return expect(
execTestScript(
`
pw.expect(5).not.toHaveLength(0)
pw.expect(true).not.toHaveLength(0)
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: "Expected toHaveLength to be called for an array or string" },
{ status: "error", message: "Expected toHaveLength to be called for an array or string" },
]
})
])
})
test("gives an error if toHaveLength parameter is not a number without negation", () => {
return expect(
execTestScript(
`
pw.expect([1, 2, 3, 4]).toHaveLength("a")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: "Argument for toHaveLength should be a number" },
],
})
])
})
test("gives an error if toHaveLength parameter is not a number with negation", () => {
return expect(
execTestScript(
`
pw.expect([1, 2, 3, 4]).not.toHaveLength("a")
`,
fakeResponse
)()
).resolves.toEqualRight([
expect.objectContaining({
expectResults: [
{ status: "error", message: "Argument for toHaveLength should be a number" },
],
})
])
})
})

View File

@@ -0,0 +1,59 @@
import { execTestScript, TestResponse } from "../../test-runner"
const fakeResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
}
describe("execTestScript function behavior", () => {
test("returns a resolved promise for a valid test scripts with all green", () => {
return expect(
execTestScript(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;
pw.expect(size).toBe(1000);
pw.expect(size - 500).toBe(500);
pw.expect(size * 4).toBe(4000);
pw.expect(size / 4).toBe(250);
});
`,
fakeResponse
)()
).resolves.toBeRight();
})
test("resolves for tests with failed expectations", () => {
return expect(
execTestScript(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;
pw.expect(size).toBe(1000);
pw.expect(size - 500).not.toBe(500);
pw.expect(size * 4).toBe(4000);
pw.expect(size / 4).not.toBe(250);
});
`, fakeResponse
)()
).resolves.toBeRight()
})
// TODO: We need a more concrete behavior for this
test("rejects for invalid syntax on tests", () => {
return expect(
execTestScript(
`
pw.test("Arithmetic operations", () => {
const size = 500 + 500;
pw.expect(size).
pw.expect(size - 500).not.toBe(500);
pw.expect(size * 4).toBe(4000);
pw.expect(size / 4).not.toBe(250);
});
`, fakeResponse
)()
).resolves.toBeLeft()
})
})

View File

@@ -0,0 +1,57 @@
import { match } from "fp-ts/lib/Either"
import { pipe } from "fp-ts/lib/function"
import * as QuickJS from "quickjs-emscripten"
import { marshalObjectToVM } 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", () => {
const testObj = {
a: 1
}
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
marshalObjectToVM(vm, testObj),
match(
(e) => null,
(result) => result
)
)
expect(objVMHandle).not.toBeNull()
expect(vm.dump(objVMHandle!)).toEqual(testObj)
objVMHandle!.dispose()
})
test("fails marshalling cyclic object into vm", () => {
const testObj = {
a: 1,
b: null as any
}
testObj.b = testObj
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
marshalObjectToVM(vm, testObj),
match(
(e) => null,
(result) => result
)
)
expect(objVMHandle).toBeNull()
})
})

View File

@@ -0,0 +1,55 @@
import { runTestScript } from "./index"
import { TestResponse } from "./test-runner"
const dummyResponse: TestResponse = {
status: 200,
body: "hoi",
headers: []
};
// eslint-disable-next-line prettier/prettier
(async () => {
console.dir(
await runTestScript(
`
pw.test("Arithmetic operations and toBe", () => {
const size = 500 + 500;
pw.expect(size).toBe(1000);
pw.expect(size - 500).toBe(500);
pw.expect(size * 4).toBe(4000);
pw.expect(size / 4).toBe(250);
});
pw.test("toBeLevelxxx", () => {
pw.expect(200).toBeLevel2xx();
pw.expect(204).toBeLevel2xx();
pw.expect(300).not.toBeLevel2xx();
pw.expect(300).toBeLevel3xx();
pw.expect(304).toBeLevel3xx();
pw.expect(204).not.toBeLevel3xx();
pw.expect(401).toBeLevel4xx();
pw.expect(404).toBeLevel4xx();
pw.expect(204).not.toBeLevel4xx();
pw.expect(501).toBeLevel5xx();
pw.expect(504).toBeLevel5xx();
pw.expect(204).not.toBeLevel5xx();
});
pw.test("toBeType", () => {
pw.expect("hello").toBeType("string");
pw.expect(10).toBeType("number");
pw.expect(true).toBeType("boolean");
pw.expect("deffonotanumber").not.toBeType("number");
});
pw.test("toHaveLength", () => {
const arr = [1, 2, 3];
pw.expect(arr).toHaveLength(3);
pw.expect(arr).not.toHaveLength(4);
});
`,
dummyResponse
),
{
depth: 100,
}
)
})()

View File

@@ -0,0 +1 @@
import '@relmify/jest-fp-ts';

View File

@@ -0,0 +1,26 @@
import { pipe } from "fp-ts/lib/function"
import { chain, right } from "fp-ts/lib/TaskEither"
import { execPreRequestScript } from "./preRequest"
import { execTestScript, TestResponse, TestDescriptor as _TestDescriptor } from "./test-runner"
export type TestDescriptor = _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,
response: TestResponse
) => pipe(
execTestScript(testScript, response),
chain((results) => right(results[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 envirionment 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

View File

@@ -0,0 +1,77 @@
import { pipe } from "fp-ts/lib/function";
import { chain, TaskEither, tryCatch, right, left } from "fp-ts/lib/TaskEither";
import * as qjs from "quickjs-emscripten";
import clone from "lodash/clone";
type EnvEntry = {
key: string;
value: string;
};
export const execPreRequestScript = (
preRequestScript: string,
env: EnvEntry[]
): TaskEither<string, EnvEntry[]> => pipe(
tryCatch(
async () => await qjs.getQuickJS(),
(reason) => `QuickJS initialization failed: ${reason}`
),
chain(
(QuickJS) => {
const finalEnv = clone(env)
const vm = QuickJS.createVm()
const pwHandle = vm.newObject()
const envHandle = vm.newObject()
const envSetFuncHandle = vm.newFunction("set", (keyHandle, valueHandle) => {
const key = vm.dump(keyHandle)
const value = 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")
}
const keyIndex = finalEnv.findIndex((env) => env.key === key)
if (keyIndex === -1) {
finalEnv.push({ key, value })
} else {
finalEnv[keyIndex] = { key, value }
}
return {
value: vm.undefined
}
})
vm.setProp(envHandle, "set", envSetFuncHandle)
envSetFuncHandle.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 left(`Script evaluation failed: ${errorData}`)
}
vm.dispose()
return right(finalEnv)
}
)
)

View File

@@ -0,0 +1,367 @@
import { isLeft } from "fp-ts/lib/Either"
import { pipe } from "fp-ts/lib/function"
import { TaskEither, tryCatch, chain, right, left } from "fp-ts/lib/TaskEither"
import * as qjs from "quickjs-emscripten"
import { marshalObjectToVM } 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[]
}
/**
* 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 }
}
)
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.defineProp(resultHandle, "not", {
get: () => {
return createExpectation(vm, expectVal, !negated, currTestStack)
},
})
toBeFnHandle.dispose()
toBeLevel2xxHandle.dispose()
toBeLevel3xxHandle.dispose()
toBeLevel4xxHandle.dispose()
toBeLevel5xxHandle.dispose()
toBeTypeHandle.dispose()
toHaveLengthHandle.dispose()
return resultHandle
}
export const execTestScript = (
testScript: string,
response: TestResponse
): TaskEither<string, TestDescriptor[]> => pipe(
tryCatch(
async () => await qjs.getQuickJS(),
(reason) => `QuickJS initialization failed: ${reason}`
),
chain(
// TODO: Make this more functional ?
(QuickJS) => {
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 (isLeft(responseObjHandle)) return 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()
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 left(`Script evaluation failed: ${errorData}`)
}
vm.dispose()
return right(testRunStack)
}
)
)

View File

@@ -0,0 +1,32 @@
import { Either, left, right } from "fp-ts/lib/Either";
import * as QuickJS from "quickjs-emscripten";
export function marshalObjectToVM(vm: QuickJS.QuickJSVm, obj: object): Either<string, QuickJS.QuickJSHandle> {
let jsonString
try {
jsonString = JSON.stringify(obj)
} catch (e) {
return 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 left("Marshaling failed")
}
const resultHandle = vm.unwrapResult(parseResultHandle)
vmStringHandle.dispose()
parseFuncHandle.dispose()
jsonHandle.dispose()
return right(resultHandle)
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"esModuleInterop": true,
"strict": true,
"paths": {
"~/*": ["./src/*"],
"@/*": ["./src/*"]
},
"types": [
"@types/node",
"@types/jest",
"@relmify/jest-fp-ts"
],
"outDir": "./lib/",
"rootDir": "./src/",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["./src", "./src/global.d.ts"],
"exclude": ["node_modules", "./src/__tests__", "./src/demo.ts"]
}

239
pnpm-lock.yaml generated
View File

@@ -22,6 +22,7 @@ importers:
'@babel/preset-env': ^7.15.6
'@commitlint/cli': ^13.1.0
'@commitlint/config-conventional': ^13.1.0
'@hoppscotch/js-sandbox': workspace:^1.0.0
'@nuxt/types': ^2.15.8
'@nuxt/typescript-build': ^2.1.0
'@nuxtjs/axios': ^5.13.6
@@ -63,6 +64,7 @@ importers:
eslint-plugin-vue: ^7.18.0
esprima: ^4.0.1
firebase: ^9.0.2
fp-ts: ^2.11.3
fuse.js: ^6.4.6
graphql: ^15.5.3
graphql-language-service-interface: ^2.8.4
@@ -104,6 +106,7 @@ importers:
yargs-parser: ^20.2.9
dependencies:
'@apollo/client': 3.4.12_graphql@15.5.3
'@hoppscotch/js-sandbox': link:../hoppscotch-js-sandbox
'@nuxtjs/axios': 5.13.6
'@nuxtjs/composition-api': 0.29.0_nuxt@2.15.8
'@nuxtjs/gtm': 2.4.0
@@ -119,6 +122,7 @@ importers:
core-js: 3.17.3
esprima: 4.0.1
firebase: 9.0.2
fp-ts: 2.11.3
fuse.js: 6.4.6
graphql: 15.5.3
graphql-language-service-interface: 2.8.4_graphql@15.5.3
@@ -190,6 +194,49 @@ importers:
vue-jest: 3.0.7_babel-core@7.0.0-bridge.0
worker-loader: 3.0.8
packages/hoppscotch-js-sandbox:
specifiers:
'@digitak/esrun': ^1.2.4
'@relmify/jest-fp-ts': ^1.1.1
'@types/jest': ^26.0.23
'@types/lodash': ^4.14.173
'@types/node': ^15.12.5
'@typescript-eslint/eslint-plugin': ^4.28.1
'@typescript-eslint/parser': ^4.28.1
eslint: ^7.29.0
eslint-config-prettier: ^8.3.0
eslint-plugin-prettier: ^3.4.0
fp-ts: ^2.11.3
io-ts: ^2.2.16
jest: ^27.0.6
lodash: ^4.17.21
prettier: ^2.3.2
pretty-quick: ^3.1.1
quickjs-emscripten: ^0.13.0
ts-jest: ^27.0.3
typescript: ^4.3.5
dependencies:
fp-ts: 2.11.3
lodash: 4.17.21
quickjs-emscripten: 0.13.0
devDependencies:
'@digitak/esrun': 1.2.7
'@relmify/jest-fp-ts': 1.1.1_fp-ts@2.11.3+io-ts@2.2.16
'@types/jest': 26.0.24
'@types/lodash': 4.14.173
'@types/node': 15.14.9
'@typescript-eslint/eslint-plugin': 4.31.1_e2d3c88d378335c4183365c112128ce9
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.4.3
eslint: 7.32.0
eslint-config-prettier: 8.3.0_eslint@7.32.0
eslint-plugin-prettier: 3.4.1_6e975bd57c7acf028c1a9ddbbf60c898
io-ts: 2.2.16_fp-ts@2.11.3
jest: 27.2.0
prettier: 2.4.1
pretty-quick: 3.1.1_prettier@2.4.1
ts-jest: 27.0.5_bd302350a23170fc0ef61cbda3ff9f29
typescript: 4.4.3
packages:
/@antfu/utils/0.2.4:
@@ -1706,6 +1753,21 @@ packages:
engines: {node: '>=4.0.0'}
dev: false
/@digitak/esrun/1.2.7:
resolution: {integrity: sha512-G+4t0LvYDS684U/9uOaaOb3Qvqiju2oMK/JIk9liENtDswTxwFVnpIG5vpmszIQkuexcfGGN09VJS1mkrmmPJQ==}
engines: {node: '>=14.0'}
hasBin: true
dependencies:
'@digitak/grubber': 1.0.2
anymatch: 3.1.2
chokidar: 3.5.2
esbuild: 0.12.28
dev: true
/@digitak/grubber/1.0.2:
resolution: {integrity: sha512-cufCjWJh9MEsdMZgg4MAyUYtLdwboyiqIYir+/zroxWQ9XFqrYLin/tj1eJuH3UyW2qoy5RWXF4TbteDZ3zd7Q==}
dev: true
/@endemolshinegroup/cosmiconfig-typescript-loader/1.0.2_4f9b016a9697d58bd127ac5ffca32a8d:
resolution: {integrity: sha512-ZHkXKq2XFFmAUdmSZrmqUSIrRM4O9gtkdpxMmV+LQl7kScUnbo6pMnXu6+FTDgZ12aW6SDoZoOJfS56WD+Eu6A==}
engines: {node: '>=8.0.0', yarn: '>=1.3.0'}
@@ -2834,6 +2896,17 @@ packages:
- supports-color
dev: true
/@jest/types/26.6.2:
resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==}
engines: {node: '>= 10.14.2'}
dependencies:
'@types/istanbul-lib-coverage': 2.0.3
'@types/istanbul-reports': 3.0.1
'@types/node': 16.9.2
'@types/yargs': 15.0.14
chalk: 4.1.2
dev: true
/@jest/types/27.1.1:
resolution: {integrity: sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -3735,6 +3808,19 @@ packages:
resolution: {integrity: sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=}
dev: false
/@relmify/jest-fp-ts/1.1.1_fp-ts@2.11.3+io-ts@2.2.16:
resolution: {integrity: sha512-cuYK27gjDoEZIzvUuLRvgmqp/t5BlGWMrsVj+kzGlsgYny9xYCAMMRBd38HcFciG6aVTVKrLKqsYNidvU+4vzA==}
peerDependencies:
fp-ts: 2.x
io-ts: 2.x
dependencies:
expect: 26.6.2
fp-ts: 2.11.3
io-ts: 2.2.16_fp-ts@2.11.3
jest-get-type: 26.3.0
jest-matcher-utils: 26.6.2
dev: true
/@samverschueren/stream-to-observable/0.3.1_rxjs@6.6.7:
resolution: {integrity: sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==}
engines: {node: '>=6'}
@@ -4079,6 +4165,13 @@ packages:
'@types/istanbul-lib-report': 3.0.0
dev: true
/@types/jest/26.0.24:
resolution: {integrity: sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==}
dependencies:
jest-diff: 26.6.2
pretty-format: 26.6.2
dev: true
/@types/jest/27.0.1:
resolution: {integrity: sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==}
dependencies:
@@ -4175,6 +4268,10 @@ packages:
resolution: {integrity: sha512-hcTWqk7DR/HrN9Xe7AlJwuCaL13Vcd9/g/T54YrJz4Q3ESM5mr33YCzW2bOfzSIc3aZMeGBvbLGvgN6mIJ0I5Q==}
dev: false
/@types/node/15.14.9:
resolution: {integrity: sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==}
dev: true
/@types/node/16.9.2:
resolution: {integrity: sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==}
@@ -4372,6 +4469,12 @@ packages:
resolution: {integrity: sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==}
dev: true
/@types/yargs/15.0.14:
resolution: {integrity: sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==}
dependencies:
'@types/yargs-parser': 20.2.1
dev: true
/@types/yargs/16.0.4:
resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==}
dependencies:
@@ -7762,6 +7865,11 @@ packages:
streamsearch: 0.1.2
dev: false
/diff-sequences/26.6.2:
resolution: {integrity: sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==}
engines: {node: '>= 10.14.2'}
dev: true
/diff-sequences/27.0.6:
resolution: {integrity: sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -8264,6 +8372,23 @@ packages:
- supports-color
dev: true
/eslint-plugin-prettier/3.4.1_6e975bd57c7acf028c1a9ddbbf60c898:
resolution: {integrity: sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==}
engines: {node: '>=6.0.0'}
peerDependencies:
eslint: '>=5.0.0'
eslint-config-prettier: '*'
prettier: '>=1.13.0'
peerDependenciesMeta:
eslint-config-prettier:
optional: true
dependencies:
eslint: 7.32.0
eslint-config-prettier: 8.3.0_eslint@7.32.0
prettier: 2.4.1
prettier-linter-helpers: 1.0.0
dev: true
/eslint-plugin-prettier/4.0.0_6e975bd57c7acf028c1a9ddbbf60c898:
resolution: {integrity: sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==}
engines: {node: '>=6.0.0'}
@@ -8600,6 +8725,18 @@ packages:
snapdragon: 0.8.2
to-regex: 3.0.2
/expect/26.6.2:
resolution: {integrity: sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==}
engines: {node: '>= 10.14.2'}
dependencies:
'@jest/types': 26.6.2
ansi-styles: 4.3.0
jest-get-type: 26.3.0
jest-matcher-utils: 26.6.2
jest-message-util: 26.6.2
jest-regex-util: 26.0.0
dev: true
/expect/27.2.0:
resolution: {integrity: sha512-oOTbawMQv7AK1FZURbPTgGSzmhxkjFzoARSvDjOMnOpeWuYQx1tP6rXu9MIX5mrACmyCAM7fSNP8IJO2f1p0CQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -9064,6 +9201,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/fp-ts/2.11.3:
resolution: {integrity: sha512-qHI5iaVSFNFmdl6yDensWfFMk32iafAINCnqx8m486DV1+Jht/bTnA9CyahL+Xm7h2y3erinviVBIAWvv5bPYw==}
dev: false
/fragment-cache/0.2.1:
resolution: {integrity: sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=}
engines: {node: '>=0.10.0'}
@@ -10291,6 +10432,14 @@ packages:
dependencies:
loose-envify: 1.4.0
/io-ts/2.2.16_fp-ts@2.11.3:
resolution: {integrity: sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q==}
peerDependencies:
fp-ts: ^2.5.0
dependencies:
fp-ts: 2.11.3
dev: true
/ip/1.1.5:
resolution: {integrity: sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=}
dev: false
@@ -10879,6 +11028,16 @@ packages:
- utf-8-validate
dev: true
/jest-diff/26.6.2:
resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==}
engines: {node: '>= 10.14.2'}
dependencies:
chalk: 4.1.2
diff-sequences: 26.6.2
jest-get-type: 26.3.0
pretty-format: 26.6.2
dev: true
/jest-diff/27.2.0:
resolution: {integrity: sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -10937,6 +11096,11 @@ packages:
jest-util: 27.2.0
dev: true
/jest-get-type/26.3.0:
resolution: {integrity: sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==}
engines: {node: '>= 10.14.2'}
dev: true
/jest-get-type/27.0.6:
resolution: {integrity: sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -10996,6 +11160,16 @@ packages:
pretty-format: 27.2.0
dev: true
/jest-matcher-utils/26.6.2:
resolution: {integrity: sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==}
engines: {node: '>= 10.14.2'}
dependencies:
chalk: 4.1.2
jest-diff: 26.6.2
jest-get-type: 26.3.0
pretty-format: 26.6.2
dev: true
/jest-matcher-utils/27.2.0:
resolution: {integrity: sha512-F+LG3iTwJ0gPjxBX6HCyrARFXq6jjiqhwBQeskkJQgSLeF1j6ui1RTV08SR7O51XTUhtc8zqpDj8iCG4RGmdKw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -11006,6 +11180,21 @@ packages:
pretty-format: 27.2.0
dev: true
/jest-message-util/26.6.2:
resolution: {integrity: sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==}
engines: {node: '>= 10.14.2'}
dependencies:
'@babel/code-frame': 7.14.5
'@jest/types': 26.6.2
'@types/stack-utils': 2.0.1
chalk: 4.1.2
graceful-fs: 4.2.8
micromatch: 4.0.4
pretty-format: 26.6.2
slash: 3.0.0
stack-utils: 2.0.5
dev: true
/jest-message-util/27.2.0:
resolution: {integrity: sha512-y+sfT/94CiP8rKXgwCOzO1mUazIEdEhrLjuiu+RKmCP+8O/TJTSne9dqQRbFIHBtlR2+q7cddJlWGir8UATu5w==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -11041,6 +11230,11 @@ packages:
jest-resolve: 27.2.0
dev: true
/jest-regex-util/26.0.0:
resolution: {integrity: sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==}
engines: {node: '>= 10.14.2'}
dev: true
/jest-regex-util/27.0.6:
resolution: {integrity: sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -14071,6 +14265,16 @@ packages:
renderkid: 2.0.7
dev: false
/pretty-format/26.6.2:
resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==}
engines: {node: '>= 10'}
dependencies:
'@jest/types': 26.6.2
ansi-regex: 5.0.1
ansi-styles: 4.3.0
react-is: 17.0.2
dev: true
/pretty-format/27.2.0:
resolution: {integrity: sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -14429,6 +14633,10 @@ packages:
engines: {node: '>=10'}
dev: true
/quickjs-emscripten/0.13.0:
resolution: {integrity: sha512-+8oz1u1Xs1I/FlQuH7p0xb9/GTPNvcLDTbfSmIgUoe4wtFrDtgxlHekmEaToXZyFJ3/rJYzjuG4djjfXTn+3lA==}
dev: false
/randombytes/2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies:
@@ -16455,6 +16663,37 @@ packages:
yargs-parser: 20.2.9
dev: true
/ts-jest/27.0.5_bd302350a23170fc0ef61cbda3ff9f29:
resolution: {integrity: sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
hasBin: true
peerDependencies:
'@babel/core': '>=7.0.0-beta.0 <8'
'@types/jest': ^27.0.0
babel-jest: '>=27.0.0 <28'
jest: ^27.0.0
typescript: '>=3.8 <5.0'
peerDependenciesMeta:
'@babel/core':
optional: true
'@types/jest':
optional: true
babel-jest:
optional: true
dependencies:
'@types/jest': 26.0.24
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 27.2.0
jest-util: 27.2.0
json5: 2.2.0
lodash: 4.17.21
make-error: 1.3.6
semver: 7.3.5
typescript: 4.4.3
yargs-parser: 20.2.9
dev: true
/ts-loader/8.3.0_typescript@4.2.4:
resolution: {integrity: sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==}
engines: {node: '>=10.0.0'}