367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
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)
|
|
}
|
|
)
|
|
) |