zzapi
Version:
zzAPI is a REST API testing and documentation tool set. It is an open-source Postman alternative.
1 lines • 90.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/parseBundle.ts","../src/utils/typeUtils.ts","../src/mergeData.ts","../src/checkTypes.ts","../src/variables.ts","../src/variableParser.ts","../src/replaceVars.ts","../src/executeRequest.ts","../src/constructCurl.ts","../src/runTests.ts","../src/captureVars.ts","../src/convertPostman.ts"],"sourcesContent":["import * as YAML from \"yaml\";\n\nimport { isDict } from \"./utils/typeUtils\";\n\nimport { RawRequest, RequestSpec, RequestPosition, Common } from \"./models\";\nimport { getMergedData } from \"./mergeData\";\nimport { checkCommonType, validateRawRequest } from \"./checkTypes\";\n\nconst VALID_KEYS: { [key: string]: boolean } = {\n requests: true,\n common: true,\n variables: true,\n};\n\n// returning as an array does not enforce typechecking, so we return as an object\nfunction getRawRequests(doc: string): {\n rawRequests: { [name: string]: RawRequest };\n commonData: Common;\n} {\n let parsedData = YAML.parse(doc);\n if (!isDict(parsedData)) {\n throw new Error(\"Bundle could not be parsed. Is your bundle a valid YAML document?\");\n }\n\n for (const key in parsedData) {\n if (!VALID_KEYS[key]) {\n throw new Error(`Invalid key: ${key} in bundle. Only ${Object.keys(VALID_KEYS)} are allowed.`);\n }\n }\n\n let commonData = parsedData.common;\n if (commonData !== undefined) {\n const error = checkCommonType(commonData);\n if (error !== undefined) throw new Error(`error in common: ${error}`);\n } else {\n commonData = {};\n }\n const allRequests = parsedData.requests;\n if (!isDict(allRequests)) {\n throw new Error(\"requests must be a dictionary in the bundle.\");\n }\n return { rawRequests: allRequests, commonData: commonData };\n}\n\nfunction checkAndMergeRequest(\n commonData: Common,\n allRequests: { [name: string]: RawRequest },\n name: string,\n): RequestSpec {\n let request = allRequests[name];\n if (request === undefined) throw new Error(`Request ${name} is not defined in this bundle`);\n\n request.name = name;\n const error = validateRawRequest(request);\n if (error !== undefined) throw new Error(`error in request '${name}': ${error}`);\n\n return getMergedData(commonData, request);\n}\n\n/**\n * @param document the yaml document to parse to form the requests\n * @returns An array of RequestPosition objects\n */\nexport function getRequestPositions(document: string): RequestPosition[] {\n let positions: RequestPosition[] = [];\n\n const lineCounter = new YAML.LineCounter();\n let doc = YAML.parseDocument(document, { lineCounter });\n\n if (!YAML.isMap(doc.contents)) {\n return positions;\n }\n let contents = doc.contents as YAML.YAMLMap;\n\n function getPosition(key: YAML.Scalar, name?: string) {\n const start = key.range?.[0] as number;\n const end = key.range?.[1] as number;\n const pos: RequestPosition = {\n name: name,\n start: lineCounter.linePos(start),\n end: lineCounter.linePos(end),\n };\n\n return pos;\n }\n\n contents.items.forEach((topLevelItem) => {\n if (!YAML.isMap(topLevelItem.value)) {\n return;\n }\n let key = topLevelItem.key as YAML.Scalar;\n if (key.value !== \"requests\") {\n return;\n }\n\n positions.push(getPosition(key));\n\n let requests = topLevelItem.value as YAML.YAMLMap;\n requests.items.forEach((request) => {\n if (!YAML.isMap(request.value)) {\n return;\n }\n let key = request.key as YAML.Scalar;\n const name = key.value as string;\n\n positions.push(getPosition(key, name));\n });\n });\n\n return positions;\n}\n\n/**\n * @param document the yaml document to parse to form the requests\n * @returns An object of type { [name: string]: RequestSpec } where each value is the data\n * of a request of the name key\n */\nexport function getAllRequestSpecs(document: string): { [name: string]: RequestSpec } {\n const { rawRequests: allRequests, commonData: commonData } = getRawRequests(document);\n\n const requests: { [name: string]: RequestSpec } = {};\n for (const name in allRequests) {\n requests[name] = checkAndMergeRequest(commonData, allRequests, name);\n }\n return requests;\n}\n\n/**\n * @param document the yaml document to parse to form the requests\n * @returns An object of type RequestSpec\n */\nexport function getRequestSpec(document: string, name: string): RequestSpec {\n const { rawRequests: allRequests, commonData: commonData } = getRawRequests(document);\n\n return checkAndMergeRequest(commonData, allRequests, name);\n}\n","export function isArrayOrDict(obj: any) {\n return typeof obj == \"object\" && !(obj instanceof Date) && obj !== null;\n}\n\nexport function isDict(obj: any) {\n return isArrayOrDict(obj) && !Array.isArray(obj);\n}\n\nexport function getDescriptiveType(obj: any): string {\n if (obj === null) return \"null\";\n if (Array.isArray(obj)) return \"array\";\n if (obj instanceof Date) return \"instanceof Date\";\n if (typeof obj === \"object\") return \"dict\"; // if none of the above but object, it is map/dict\n return typeof obj;\n}\n\nexport function getStringIfNotScalar(data: any): Exclude<any, object> {\n if (typeof data !== \"object\") return data;\n return JSON.stringify(data);\n}\n\nexport function getStringValueIfDefined<\n T extends undefined | Exclude<any, undefined>,\n R = T extends undefined ? undefined : string,\n>(value: T): R {\n if (typeof value === \"undefined\") return undefined as R;\n if (typeof value === \"object\") return JSON.stringify(value) as R; // handles dicts, arrays, null, date (all obj)\n return value.toString() as R; // handles scalars\n}\n\nexport function getStrictStringValue(value: any): string {\n if (typeof value === \"undefined\") return \"undefined\";\n return getStringValueIfDefined(value);\n}\n\nexport function isString(value: any): boolean {\n return typeof value === \"string\" || value instanceof String;\n}\n\nexport function isFilePath(value: any): boolean {\n if (!isString(value)) {\n return false;\n }\n const fileRegex = /file:\\/\\/([^\\s]+)/g;\n return fileRegex.test(value);\n}\n\nexport function hasFile(formValues: any): boolean {\n if (!formValues) {\n return false;\n }\n for (const formValue of formValues) {\n if (isFilePath(formValue.value)) {\n return true;\n }\n }\n return false;\n}\n","import {\n RequestSpec,\n RawRequest,\n Common,\n Header,\n Param,\n Tests,\n Captures,\n RawOptions,\n Options,\n RawParams,\n RawHeaders,\n RawTests,\n RawSetVars,\n SetVar,\n} from \"./models\";\n\nfunction paramObjectToArray(params: object): Param[] {\n const paramArray: Param[] = [];\n Object.entries(params).forEach(([name, value]) => {\n if (Array.isArray(value)) {\n value.forEach((v) => {\n paramArray.push({ name, value: v });\n });\n } else {\n paramArray.push({ name, value });\n }\n });\n return paramArray;\n}\n\nfunction getMergedParams(commonParams: RawParams, requestParams: RawParams): Param[] {\n let mixedParams: Param[] = [];\n\n if (commonParams) {\n if (Array.isArray(commonParams)) {\n mixedParams = mixedParams.concat(commonParams);\n } else {\n mixedParams = mixedParams.concat(paramObjectToArray(commonParams));\n }\n }\n if (requestParams) {\n if (Array.isArray(requestParams)) {\n mixedParams = mixedParams.concat(requestParams);\n } else {\n mixedParams = mixedParams.concat(paramObjectToArray(requestParams));\n }\n }\n return mixedParams;\n}\n\nfunction getMergedHeaders(\n commonHeaders: RawHeaders,\n requestHeaders: RawHeaders,\n): { [name: string]: string } {\n if (Array.isArray(commonHeaders)) {\n commonHeaders = getArrayHeadersAsObject(commonHeaders);\n }\n if (Array.isArray(requestHeaders)) {\n requestHeaders = getArrayHeadersAsObject(requestHeaders);\n }\n commonHeaders = withLowerCaseKeys(commonHeaders);\n requestHeaders = withLowerCaseKeys(requestHeaders);\n\n return Object.assign({}, commonHeaders, requestHeaders);\n}\n\nfunction getMergedOptions(cOptions: RawOptions = {}, rOptions: RawOptions = {}): Options {\n const options = Object.assign(cOptions, rOptions);\n\n const follow = options.follow === true;\n const verifySSL = options.verifySSL === true;\n const keepRawJSON = options.keepRawJSON === true;\n const showHeaders = options.showHeaders === true;\n const rawParams = options.rawParams === true;\n const stopOnFailure = options.stopOnFailure === true;\n\n return { follow, verifySSL, keepRawJSON, showHeaders, rawParams, stopOnFailure };\n}\n\nfunction getMergedSetVars(\n setvars: RawSetVars = {},\n captures: Captures = {},\n): { mergedVars: SetVar[]; hasJsonVars: boolean } {\n const mergedVars: SetVar[] = [];\n let hasJsonVars = false;\n\n // captures is the old way, deprecated, but we still support it\n if (captures.body) {\n mergedVars.push({ varName: captures.body, type: \"body\", spec: \"\" });\n }\n if (captures.status) {\n mergedVars.push({ varName: captures.status, type: \"status\", spec: \"\" });\n }\n if (captures.headers) {\n for (const header in captures.headers) {\n mergedVars.push({\n varName: captures.headers[header],\n type: \"header\",\n spec: header,\n });\n }\n }\n if (captures.json) {\n for (const path in captures.json) {\n hasJsonVars = true;\n mergedVars.push({\n varName: captures.json[path],\n type: \"json\",\n spec: path,\n });\n }\n }\n\n // Regular new way of defining variable captures: setvars\n for (const varName in setvars) {\n let spec = setvars[varName];\n let type: \"body\" | \"json\" | \"status\" | \"header\";\n if (spec.startsWith(\"$.\")) {\n type = \"json\";\n hasJsonVars = true;\n } else if (spec.startsWith(\"$h.\")) {\n type = \"header\";\n spec = spec.replace(/^\\$h\\./, \"\");\n } else if (spec === \"status\" || spec === \"body\") {\n type = spec;\n } else {\n continue;\n }\n mergedVars.push({ varName, type, spec });\n }\n\n return { mergedVars: mergedVars, hasJsonVars: hasJsonVars };\n}\n\n/*\n * json and header tests can be specified at the root level of 'tests' using\n * $. and $h. prefixes, as this is more convenient when specifying them.\n * We merge these into tests.json and tests.headers respectively.\n */\nexport function mergePrefixBasedTests(tests: RawTests) {\n if (!tests.json) tests.json = {};\n if (!tests.headers) tests.headers = {};\n for (const key of Object.keys(tests)) {\n if (key.startsWith(\"$.\")) {\n tests.json[key] = tests[key];\n delete tests[key];\n } else if (key.startsWith(\"$h.\")) {\n const headerName = key.replace(/^\\$h\\./, \"\");\n tests.headers[headerName] = tests[key];\n delete tests[key];\n }\n }\n}\n\nfunction getMergedTests(\n cTests: RawTests = {},\n rTests: RawTests = {},\n): { mergedTests: Tests; hasJsonTests: boolean } {\n // Convert $. and h. at root level into headers and json keys\n mergePrefixBasedTests(cTests);\n mergePrefixBasedTests(rTests);\n\n cTests.headers = withLowerCaseKeys(cTests.headers);\n rTests.headers = withLowerCaseKeys(rTests.headers);\n\n // We override status and body but we merge (combine) headers and json tests\n let mergedData: Tests = {\n status: rTests.status || cTests.status,\n body: rTests.body || cTests.body,\n headers: Object.assign({}, cTests.headers, rTests.headers),\n json: Object.assign({}, cTests.json, rTests.json),\n };\n\n return { mergedTests: mergedData, hasJsonTests: Object.keys(mergedData.json).length > 0 };\n}\n\nfunction getArrayHeadersAsObject(objectSet: Header[] | undefined): { [key: string]: string } {\n if (!objectSet) return {};\n\n let finalObject: { [key: string]: string } = {};\n\n objectSet.forEach((currObj) => {\n const key = currObj.name;\n const value = currObj.value;\n\n finalObject[key] = value;\n });\n\n return finalObject;\n}\n\nfunction withLowerCaseKeys(obj: { [key: string]: any } | undefined): { [key: string]: any } {\n if (!obj) return {};\n\n let newObj: { [key: string]: any } = {};\n for (const key in obj) {\n newObj[key.toLowerCase()] = obj[key];\n }\n\n return newObj;\n}\n\nexport function getMergedData(commonData: Common, requestData: RawRequest): RequestSpec {\n const name = requestData.name;\n\n const method = requestData.method;\n const params = getMergedParams(commonData.params, requestData.params);\n const headers = getMergedHeaders(commonData.headers, requestData.headers);\n const body = requestData.body;\n const formValues = getMergedParams([], requestData.formValues);\n const options = getMergedOptions(commonData.options, requestData.options);\n\n const { mergedTests: tests, hasJsonTests: hasJsonTests } = getMergedTests(\n commonData?.tests,\n requestData.tests,\n );\n const { mergedVars: setvars, hasJsonVars: hasJsonVars } = getMergedSetVars(\n requestData.setvars,\n requestData.capture,\n );\n\n const mergedData: RequestSpec = {\n name,\n httpRequest: {\n baseUrl: commonData?.baseUrl,\n url: requestData.url,\n method,\n params,\n headers,\n body,\n formValues: formValues.length > 0 ? formValues : undefined,\n },\n options,\n tests,\n setvars,\n expectJson: hasJsonTests || hasJsonVars,\n };\n\n return mergedData;\n}\n","import { isArrayOrDict, isDict, getDescriptiveType, getStrictStringValue } from \"./utils/typeUtils\";\n\nfunction checkKey(\n obj: any,\n item: string,\n key: string,\n expectedTypes: string[],\n optional: boolean,\n): string | undefined {\n if (!optional && !obj.hasOwnProperty(key)) {\n return `${key} key must be present in each ${item} item`;\n } else if (obj.hasOwnProperty(key)) {\n if (\n !expectedTypes.some((type) => (type === \"null\" && obj[key] === null) || typeof obj[key] === type)\n ) {\n return `${key} of ${item} must have one of ${expectedTypes} value, found ${typeof obj[key]}`;\n }\n }\n return undefined;\n}\n\nfunction checkObjIsDict(obj: any, item: string): string | undefined {\n if (!isDict(obj)) {\n return `${item} item must be a dict: found ${getDescriptiveType(obj)}`;\n } else {\n return undefined;\n }\n}\n\nfunction checkHeaderItem(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"header\");\n if (ret !== undefined) return ret;\n\n ret = checkKey(obj, \"header\", \"name\", [\"string\"], false);\n if (ret !== undefined) return ret;\n ret = checkKey(obj, \"header\", \"value\", [\"string\", \"number\", \"boolean\", \"null\"], false);\n if (ret !== undefined) return ret;\n\n return undefined;\n}\n\nfunction checkParamItem(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"param\");\n if (ret !== undefined) return ret;\n\n ret = checkKey(obj, \"param\", \"name\", [\"string\"], true);\n if (ret !== undefined) return ret;\n // value need not exist, but if it does, it can be anything so I do not bother checking\n\n return undefined;\n}\n\nfunction checkHeadersParamsOptionsTestsCaptures(obj: any): string | undefined {\n if (obj.hasOwnProperty(\"headers\")) {\n const headers = obj.headers;\n if (!isArrayOrDict(headers)) {\n return `Headers must be an array or a dictionary: found ${typeof headers}`;\n }\n if (Array.isArray(headers)) {\n for (const header of headers) {\n const headerError = checkHeaderItem(header);\n if (headerError !== undefined) {\n return `error in header item ${getStrictStringValue(header)}: ${headerError}`;\n }\n }\n } else {\n // headers are a dictionary\n for (const header in headers) {\n const headerError = checkHeaderItem({ name: header, value: headers[header] });\n if (headerError !== undefined) {\n return `error in header item ${getStrictStringValue(header)}: ${headerError}`;\n }\n }\n }\n }\n if (obj.hasOwnProperty(\"params\")) {\n const params = obj.params;\n if (!isArrayOrDict(params)) {\n return `Params must be an array or a dictionary: found ${typeof params}`;\n }\n if (Array.isArray(params)) {\n for (const param of params) {\n const paramError = checkParamItem(param);\n if (paramError !== undefined) {\n return `error in param item ${getStrictStringValue(param)}: ${paramError}`;\n }\n }\n }\n }\n if (obj.hasOwnProperty(\"options\")) {\n const optionsError = checkOptions(obj.options);\n if (optionsError !== undefined) return `error in options: ${optionsError}`;\n }\n if (obj.hasOwnProperty(\"tests\")) {\n const testsError = checkTests(obj.tests);\n if (testsError !== undefined) return `error in tests: ${testsError}`;\n }\n if (obj.hasOwnProperty(\"capture\")) {\n const capturesError = checkCaptures(obj.capture);\n if (capturesError !== undefined) return `error in captures: ${capturesError}`;\n }\n\n return undefined;\n}\n\nfunction checkTests(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"tests\");\n if (ret !== undefined) return ret;\n\n if (obj.hasOwnProperty(\"json\")) {\n ret = checkObjIsDict(obj.json, \"JSON tests\");\n if (ret !== undefined) return ret;\n }\n if (obj.hasOwnProperty(\"body\") && !(isDict(obj.body) || typeof obj.body === \"string\")) {\n return `body tests item must be a dict or string: found ${getDescriptiveType(obj.body)}`;\n }\n if (obj.hasOwnProperty(\"status\") && !(isDict(obj.status) || typeof obj.status === \"number\")) {\n return `status tests item must be a dict or number: found ${getDescriptiveType(obj.status)}`;\n }\n if (obj.hasOwnProperty(\"headers\")) {\n ret = checkObjIsDict(obj.headers, \"header tests\");\n if (ret !== undefined) return ret;\n }\n\n return undefined;\n}\n\nfunction checkCaptures(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"captures\");\n if (ret !== undefined) return ret;\n\n if (obj.hasOwnProperty(\"json\")) {\n ret = checkObjIsDict(obj.json, \"JSON captures\");\n return ret;\n }\n\n ret = checkKey(obj, \"captures\", \"body\", [\"string\"], true);\n if (ret !== undefined) return ret;\n ret = checkKey(obj, \"captures\", \"status\", [\"string\"], true);\n if (ret !== undefined) return ret;\n\n if (obj.hasOwnProperty(\"headers\")) {\n ret = checkObjIsDict(obj.headers, \"header captures\");\n if (ret !== undefined) return ret;\n }\n\n return undefined;\n}\n\nconst VALID_OPTIONS: { [type: string]: boolean } = {\n follow: true,\n verifySSL: true,\n keepRawJSON: true,\n showHeaders: true,\n rawParams: true,\n stopOnFailure: true,\n};\nfunction checkOptions(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"options\");\n if (ret !== undefined) return ret;\n\n for (const key in obj) {\n if (VALID_OPTIONS[key]) {\n ret = checkKey(obj, \"options\", key, [\"boolean\"], true);\n if (ret !== undefined) return ret;\n } else {\n return `options must be among ${Object.keys(VALID_OPTIONS)}: found ${key}`;\n }\n }\n\n return undefined;\n}\n\nexport function checkCommonType(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"common\");\n if (ret !== undefined) return ret;\n\n ret = checkKey(obj, \"common\", \"baseUrl\", [\"string\"], true);\n if (ret !== undefined) return ret;\n\n ret = checkHeadersParamsOptionsTestsCaptures(obj);\n if (ret !== undefined) return ret;\n\n return undefined;\n}\n\n// creating it as an object for faster access\nconst VALID_METHODS: { [type: string]: boolean } = {\n options: true,\n get: true,\n post: true,\n put: true,\n patch: true,\n head: true,\n delete: true,\n trace: true,\n};\nexport function validateRawRequest(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"request\");\n if (ret !== undefined) return ret;\n\n ret = checkHeadersParamsOptionsTestsCaptures(obj);\n if (ret !== undefined) return ret;\n\n ret = checkKey(obj, \"request\", \"url\", [\"string\"], false);\n if (ret !== undefined) return ret;\n\n if (!obj.hasOwnProperty(\"method\")) {\n return `method key must be present in each request item`;\n } else {\n if (typeof obj.method !== \"string\") {\n return `value of method key must be a string`;\n } else {\n const methodToPass = obj.method.toLowerCase();\n if (!VALID_METHODS[methodToPass])\n return `method key must have value among ${Object.keys(VALID_METHODS)}: found ${methodToPass}`;\n }\n }\n\n if (obj.hasOwnProperty(\"formValues\") && obj.hasOwnProperty(\"body\")) {\n return `both body and formValues can't be present in the same request.`;\n }\n\n if (obj.hasOwnProperty(\"method\") && obj[\"method\"] == \"GET\" && obj.hasOwnProperty(\"formValues\")) {\n return `formValues can't be used with method GET`;\n }\n\n if (obj.hasOwnProperty(\"method\") && obj[\"method\"] == \"GET\" && obj.hasOwnProperty(\"body\")) {\n return `body can't be used with method GET`;\n }\n\n return undefined;\n}\n\nexport function checkVariables(obj: any): string | undefined {\n let ret = checkObjIsDict(obj, \"variables\");\n if (ret !== undefined) return ret;\n\n for (const key in obj) {\n if (typeof key !== \"string\") return `Environment names must be a string: ${key} is not a string`;\n\n const variables = obj[key];\n ret = checkObjIsDict(obj, `variables environment ${key}`);\n if (ret !== undefined) return ret;\n\n for (const varName in variables) {\n if (typeof varName !== \"string\") {\n return `variable name ${varName} in env ${key} is not a string`;\n }\n }\n }\n\n return undefined;\n}\n","export type Variables = { [key: string]: any };\n\nexport class VarStore {\n loadedVariables: Variables = {};\n capturedVariables: Variables = {};\n\n getLoadedVariables(): Variables {\n return this.loadedVariables;\n }\n setLoadedVariables(vars: Variables) {\n this.loadedVariables = vars;\n }\n resetLoadedVariables(vars: Variables) {\n this.setLoadedVariables({});\n }\n mergeLoadedVariables(vars: Variables) {\n Object.assign(this.loadedVariables, vars);\n }\n\n getCapturedVariables(): Variables {\n return this.capturedVariables;\n }\n setCapturedVariables(vars: Variables) {\n this.capturedVariables = vars;\n }\n resetCapturedVariables() {\n this.setCapturedVariables({});\n }\n mergeCapturedVariables(vars: Variables) {\n Object.assign(this.capturedVariables, vars);\n }\n\n getAllVariables(): Variables {\n return Object.assign({}, this.loadedVariables, this.capturedVariables);\n }\n}\n","import * as YAML from \"yaml\";\n\nimport { isDict } from \"./utils/typeUtils\";\n\nimport { checkVariables } from \"./checkTypes\";\nimport { Variables } from \"./variables\";\n\n// we may pass an empty string if the document is not actually a bundle\nexport function getBundleVariables(doc: string | undefined): Variables {\n let parsedData = doc ? YAML.parse(doc) : {};\n if (!isDict(parsedData)) {\n throw new Error(\"Bundle could not be parsed. Is your bundle a valid YAML document?\");\n }\n\n const variables = parsedData.variables;\n if (variables !== undefined) {\n const error = checkVariables(variables);\n if (error !== undefined) throw new Error(`error in variables: ${error}`);\n\n return variables;\n } else {\n return {};\n }\n}\n\nexport function getEnvironments(bundleContent: string | undefined, varFileContents: string[]): string[] {\n const bundleEnvNames = Object.keys(getBundleVariables(bundleContent));\n\n const fileEnvNames: string[] = [];\n varFileContents.forEach((fileContent) => {\n const envs = YAML.parse(fileContent);\n if (isDict(envs)) {\n fileEnvNames.push(...Object.keys(envs));\n }\n });\n\n const uniqueNames = new Set([...bundleEnvNames, ...fileEnvNames]);\n return [...uniqueNames];\n}\n\nfunction replaceEnvironmentVariables(vars: Variables): Variables {\n const PREFIX = \"$env.\";\n\n const getVal = (val: any): any => {\n if (typeof val !== \"string\" || !val.startsWith(PREFIX)) return val;\n\n const envVarName = val.slice(PREFIX.length);\n return envVarName in process.env ? process.env[envVarName] : val;\n };\n\n const replacedVars: Variables = {};\n for (const key in vars) replacedVars[key] = getVal(vars[key]);\n\n return replacedVars;\n}\n\nexport function loadVariables(\n envName: string | undefined,\n bundleContent: string | undefined,\n varFileContents: string[],\n): Variables {\n if (!envName) return {};\n\n const allBundleVariables = getBundleVariables(bundleContent);\n const bundleVars: Variables = allBundleVariables[envName] ?? {};\n\n const envVars: Variables = {};\n varFileContents.forEach((fileContents) => {\n const parsedData = YAML.parse(fileContents);\n if (parsedData && isDict(parsedData[envName])) Object.assign(envVars, parsedData[envName]);\n });\n\n const basicVars = Object.assign({}, envVars, bundleVars);\n const vars = replaceEnvironmentVariables(basicVars);\n\n return vars;\n}\n","import { getStrictStringValue, isArrayOrDict } from \"./utils/typeUtils\";\n\nimport { RequestSpec } from \"./models\";\nimport { Variables } from \"./variables\";\n\nfunction replaceVariablesInArray(\n data: any[],\n variables: Variables,\n): { data: any[]; undefinedVars: string[] } {\n let newData: any[] = [];\n let undefs: string[] = [];\n\n data.forEach((item) => {\n const { data: newItem, undefinedVars: newUndefs } = replaceVariables(item, variables);\n\n newData.push(newItem);\n undefs.push(...newUndefs);\n });\n\n return { data: newData, undefinedVars: undefs };\n}\n\nfunction replaceVariablesInDict(\n obj: { [key: string]: any },\n variables: Variables,\n): { data: { [key: string]: any }; undefinedVars: string[] } {\n let newData: { [key: string]: any } = {};\n let undefs: string[] = [];\n\n for (const key in obj) {\n const { data: newItem, undefinedVars: newUndefs } = replaceVariables(obj[key], variables);\n\n newData[key] = newItem;\n undefs.push(...newUndefs);\n }\n\n return { data: newData, undefinedVars: undefs };\n}\n\nfunction replaceVariablesInObject(\n data: { [key: string]: any } | any[],\n variables: Variables,\n): { data: any; undefinedVars: string[] } {\n if (Array.isArray(data)) {\n return replaceVariablesInArray(data, variables);\n } else {\n return replaceVariablesInDict(data, variables);\n }\n}\n\n/**\n * (?<!\\\\) -> negative lookbehind assertion - ensures the $( is not preceded by a backslash\n * \\$\\( -> matches the sequence \\$\\( which acts as the opening sequence\n * ([_a-zA-Z]\\w*) -> capturing group for the variable name.\n * [_a-zA-Z] -> matches any underscore or letter as starting character,\n * as the variable name must not start with a number\n * \\w* -> matches any combination of word characters (letters, digits, underscore)\n * /) -> matches the closing parentheses\n * g -> global option, regex should be tested against all possible matches in the string\n *\n * Thus, it is used to match all $(variableName)\n */\nconst VAR_REGEX_WITH_BRACES = /(?<!\\\\)\\$\\(([_a-zA-Z]\\w*)\\)/g;\n\n/**\n * (?<!\\\\) -> negative lookbehind assertion - ensures the $( is not preceded by a backslash\n * \\$ -> matches the dollar sign\n * ([_a-zA-Z]\\w*) -> capturing group of the variable name\n * [_a-zA-Z] -> matches any underscore or letter as starting character\n * as the variable name must not start with a number\n * \\w* -> matches any combination of word characters (letters, digits, underscore)\n * (?=\\W|$) -> Positive lookahead assertion. Ensures the match is followed by a non-word character\n * (\\W) or the end of a line (represented by $).\n * g -> global option, regex should be tested against all possible matches in the string\n *\n * Thus, it is used to match all $variableName\n */\nconst VAR_REGEX_WITHOUT_BRACES = /(?<!\\\\)\\$([_a-zA-Z]\\w*)(?=\\W|$)/g;\n\nfunction replaceVariablesInString(\n text: string,\n variables: Variables,\n): { data: any; undefinedVars: string[] } {\n // maintaining a separate boolean instead of initially setting valueInNativeType to, say, undefined,\n // because valueInNativeType may actually end up being undefined.\n let valueInNativeType: any;\n let variableIsFullText: boolean = false;\n const undefs: string[] = [];\n\n function replaceVar(match: string, varName: any): string {\n if (typeof varName === \"string\") {\n if (variables.hasOwnProperty(varName)) {\n const varVal = variables[varName];\n if (text === match) {\n variableIsFullText = true;\n valueInNativeType = varVal;\n }\n return getStrictStringValue(varVal);\n } else {\n undefs.push(varName);\n }\n }\n return match; // if varName is not defined well (not string), or is not a valid variable\n }\n\n // todo: make a complete match regex and return native type immediately.\n const outputText = text\n .replace(VAR_REGEX_WITH_BRACES, (match, varName) => {\n return replaceVar(match, varName);\n })\n .replace(VAR_REGEX_WITHOUT_BRACES, (match) => {\n if (match.length <= 1) return match; // this would lead to an invalid slice\n const varName = match.slice(1);\n\n return replaceVar(match, varName);\n });\n\n if (variableIsFullText) {\n return { data: valueInNativeType, undefinedVars: undefs };\n } else {\n return { data: outputText, undefinedVars: undefs };\n }\n}\n\nfunction replaceVariables(data: any, variables: Variables): { data: any; undefinedVars: string[] } {\n if (isArrayOrDict(data)) {\n return replaceVariablesInObject(data, variables);\n } else if (typeof data === \"string\") {\n return replaceVariablesInString(data, variables);\n } else {\n return { data: data, undefinedVars: [] };\n }\n}\n\nexport function replaceVariablesInRequest(request: RequestSpec, variables: Variables): string[] {\n const undefs: string[] = [];\n\n type keyOfHttp = Exclude<keyof typeof request.httpRequest, \"method\">;\n const httpPropertiesToReplace: string[] = [\"baseUrl\", \"url\", \"params\", \"headers\", \"body\"];\n httpPropertiesToReplace.forEach((prop) => {\n const httpKey = prop as keyOfHttp;\n const replacedData = replaceVariables(request.httpRequest[httpKey], variables);\n request.httpRequest[httpKey] = replacedData.data;\n undefs.push(...replacedData.undefinedVars);\n });\n\n const replacedData = replaceVariables(request.tests, variables);\n request.tests = replacedData.data;\n undefs.push(...replacedData.undefinedVars);\n\n return undefs;\n}\n","import got, { Method, OptionsOfTextResponseBody } from \"got\";\n\nimport { getStringValueIfDefined, hasFile, isFilePath } from \"./utils/typeUtils\";\n\nimport { GotRequest, Param, RequestSpec } from \"./models\";\nimport { fileFromPathSync } from \"formdata-node/file-from-path\";\n\nimport { FormDataEncoder } from \"form-data-encoder\";\nimport { FormData } from \"formdata-node\";\nimport { Readable } from \"stream\";\nimport * as path from \"path\";\n\nexport function constructGotRequest(allData: RequestSpec, workingDir?: string): GotRequest {\n const completeUrl: string = getURL(\n allData.httpRequest.baseUrl,\n allData.httpRequest.url,\n getParamsForUrl(allData.httpRequest.params, allData.options.rawParams),\n );\n\n const options: OptionsOfTextResponseBody = {\n method: allData.httpRequest.method.toLowerCase() as Method,\n body: getBody(allData, workingDir),\n headers: allData.httpRequest.headers,\n followRedirect: allData.options.follow,\n https: { rejectUnauthorized: allData.options.verifySSL },\n retry: { limit: 0 },\n };\n\n return got(completeUrl, options);\n}\n\nfunction getFileFromPath(filePath: string, workingDir?: string) {\n if (workingDir) {\n filePath = path.resolve(workingDir, filePath.slice(7)); // removes <file://> prefix\n } else {\n filePath = path.resolve(filePath.slice(7)); // takes current working directory\n }\n\n const fileName = path.basename(filePath);\n return fileFromPathSync(filePath, fileName);\n}\n\nfunction constructFormUrlEncoded(request: RequestSpec) {\n const formValues = request.httpRequest.formValues;\n if (!formValues) return \"\";\n const result = new URLSearchParams();\n if (formValues) {\n request.httpRequest.headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n }\n\n for (const { name, value } of formValues) {\n result.append(name, value);\n }\n\n return result.toString();\n}\n\nfunction constructFormData(request: RequestSpec, workingDir?: string) {\n const formValues = request.httpRequest.formValues;\n if (!formValues) return;\n const multipart = new FormData();\n\n for (const fv of formValues) {\n if (isFilePath(fv.value)) {\n multipart.append(fv.name, getFileFromPath(fv.value, workingDir));\n } else {\n multipart.append(fv.name, fv.value);\n }\n }\n const fde = new FormDataEncoder(multipart);\n\n request.httpRequest.headers[\"content-type\"] = fde.contentType; //FormDataEncoder builds the actual content-type header.\n return Readable.from(fde);\n}\n\nexport function getBody(request: RequestSpec, workingDir?: string) {\n const body = request.httpRequest.body;\n const formValues = request.httpRequest.formValues;\n\n if (request.httpRequest.headers[\"content-type\"] == \"multipart/form-data\" || hasFile(formValues)) {\n return constructFormData(request, workingDir);\n }\n\n if (formValues) {\n return constructFormUrlEncoded(request);\n }\n\n return getStringValueIfDefined(body);\n}\n\nexport async function executeGotRequest(httpRequest: GotRequest): Promise<{\n response: { [key: string]: any };\n executionTime: number;\n byteLength: number;\n error: string;\n}> {\n const startTime = new Date().getTime();\n let responseObject: { [key: string]: any };\n let size: number = 0;\n let error = \"\";\n\n try {\n responseObject = await httpRequest;\n size = Buffer.byteLength(responseObject.rawBody);\n } catch (e: any) {\n const res = e.response;\n if (res) {\n responseObject = res;\n size = res.body ? Buffer.byteLength(res.body) : 0;\n } else {\n responseObject = {};\n if (e.code === \"ERR_INVALID_URL\") {\n error = `Invalid URL: ${e.input}`;\n } else if (e.name === \"CancelError\") {\n error = \"Cancelled\";\n } else {\n error = e.message || e.code;\n }\n }\n }\n const executionTime = new Date().getTime() - startTime;\n return { response: responseObject, executionTime: executionTime, byteLength: size, error: error };\n}\n\nexport function getParamsForUrl(params: Param[] | undefined, rawParams: boolean): string {\n if (!params || params.length < 1) return \"\";\n\n let paramArray: string[] = [];\n params.forEach((param) => {\n const key = param.name;\n let value = param.value;\n if (value == undefined) {\n paramArray.push(key);\n } else if (rawParams) {\n paramArray.push(`${key}=${getStringValueIfDefined(value)}`);\n } else {\n paramArray.push(`${key}=${encodeURIComponent(getStringValueIfDefined(value))}`);\n }\n });\n\n const paramString = paramArray.join(\"&\");\n return `?${paramString}`;\n}\n\nexport function getURL(baseUrl: string | undefined, url: string, paramsForUrl: string): string {\n // base url not defined, or url does not start with /, then ignore base url\n if (!baseUrl || !url.startsWith(\"/\")) return url + paramsForUrl;\n // otherwise, incorporate base url\n return baseUrl + url + paramsForUrl;\n}\n","import { getStringValueIfDefined, hasFile, isFilePath } from \"./utils/typeUtils\";\nimport { RequestSpec } from \"./models\";\nimport { getParamsForUrl, getURL } from \"./executeRequest\";\nimport path from \"path\";\n\nfunction replaceSingleQuotes<T>(value: T): T {\n if (typeof value !== \"string\") return value;\n return value.replace(/'/g, \"%27\") as T & string;\n}\n\nfunction formatCurlFormField(key: string, value: string, workingDir?: string): string {\n if (isFilePath(value)) {\n return ` --form ${key}=@\"${path.resolve(workingDir || \"\", value.slice(7))}\"`; // if workingDir not given, it takes current working directory.\n }\n return ` --form '${key}=\"${encodeURIComponent(value)}\"'`;\n}\n\nfunction getFormDataUrlEncoded(request: RequestSpec): string {\n const formValues = request.httpRequest.formValues;\n if (!formValues) return \"\";\n let result = \"\";\n\n formValues.forEach((formValue: any) => {\n result += ` --data \"${formValue.name}=${encodeURIComponent(formValue.value)}\"`;\n });\n\n return result;\n}\n\nfunction getFormDataCurlRequest(request: RequestSpec, workingDir?: string): string {\n const formValues = request.httpRequest.formValues;\n if (!formValues) return \"\";\n let result = \"\";\n for (const { name, value } of formValues) {\n result += formatCurlFormField(name, value, workingDir);\n }\n return result;\n}\n\nexport function getCurlRequest(request: RequestSpec, workingDir?: string): string {\n let curl: string = \"curl\";\n\n if (\n request.httpRequest.headers[\"content-type\"] == \"multipart/form-data\" ||\n hasFile(request.httpRequest.formValues)\n ) {\n curl += getFormDataCurlRequest(request, workingDir);\n curl += ` '${replaceSingleQuotes(\n getURL(\n request.httpRequest.baseUrl,\n request.httpRequest.url,\n getParamsForUrl(request.httpRequest.params, request.options.rawParams),\n ),\n )}'`;\n return curl;\n } else if (\n request.httpRequest.headers[\"content-type\"] == \"application/x-www-form-urlencoded\" ||\n request.httpRequest.formValues\n ) {\n curl += getFormDataUrlEncoded(request);\n curl += ` '${replaceSingleQuotes(\n getURL(\n request.httpRequest.baseUrl,\n request.httpRequest.url,\n getParamsForUrl(request.httpRequest.params, request.options.rawParams),\n ),\n )}'`;\n return curl;\n }\n\n // method\n curl += ` -X ${request.httpRequest.method.toUpperCase()}`;\n\n // headers\n if (request.httpRequest.headers !== undefined) {\n for (const header in request.httpRequest.headers) {\n curl += ` -H '${replaceSingleQuotes(`${header}: ${request.httpRequest.headers[header]}`)}'`;\n }\n }\n\n // body\n if (request.httpRequest.body !== undefined) {\n curl += ` -d '${replaceSingleQuotes(getStringValueIfDefined(request.httpRequest.body))}'`;\n }\n\n // options.follow\n if (request.options.follow) curl += \" -L\";\n\n // options.verifySSL\n if (!request.options.verifySSL) curl += \" -k\";\n\n // options.showHeaders\n if (request.options.showHeaders) curl += \" -i\";\n\n // url w/ params\n curl += ` '${replaceSingleQuotes(\n getURL(\n request.httpRequest.baseUrl,\n request.httpRequest.url,\n getParamsForUrl(request.httpRequest.params, request.options.rawParams),\n ),\n )}'`;\n\n return curl;\n}\n","import jp from \"jsonpath\";\n\nimport { getStringIfNotScalar, isDict } from \"./utils/typeUtils\";\n\nimport { Tests, ResponseData, Assertion, SpecResult, TestResult } from \"./models\";\nimport { mergePrefixBasedTests } from \"./mergeData\";\n\nconst SKIP_CLAUSE = \"$skip\",\n OPTIONS_CLAUSE = \"$options\",\n MULTI_CLAUSE = \"$multi\";\n\nfunction hasFailure(res: SpecResult): boolean {\n return res.results.some((r) => !r.pass) || res.subResults.some(hasFailure);\n}\n\nexport function runAllTests(\n tests: Tests,\n responseData: ResponseData,\n stopOnFailure: boolean,\n rootSpec: string | null = null,\n skip?: boolean,\n): SpecResult {\n const res: SpecResult = { spec: rootSpec, results: [], subResults: [] };\n if (!tests) return res;\n\n if (tests.status) {\n const expected = tests.status;\n const received = responseData.status;\n const statusResults = runTest(\"status\", expected, received, skip);\n\n res.subResults.push(statusResults);\n }\n if (stopOnFailure && hasFailure(res)) return res;\n\n for (const spec in tests.headers) {\n const expected = tests.headers[spec];\n const received = responseData.headers ? responseData.headers[spec] : \"\";\n const headerResults = runTest(spec, expected, received, skip);\n\n res.subResults.push(headerResults);\n }\n\n res.subResults.push(...runJsonTests(tests.json, responseData.json, skip));\n\n if (tests.body) {\n const expected = tests.body;\n const received = responseData.body;\n const bodyResults = runTest(\"body\", expected, received, skip);\n\n res.subResults.push(bodyResults);\n }\n\n return res;\n}\n\nfunction runJsonTests(tests: { [key: string]: Assertion }, jsonData: any, skip?: boolean): SpecResult[] {\n const results: SpecResult[] = [];\n\n for (const spec in tests) {\n const expected = tests[spec];\n let received;\n try {\n received = getValueForJSONTests(\n jsonData,\n spec,\n typeof expected === \"object\" && expected !== null && expected[MULTI_CLAUSE],\n );\n } catch (err: any) {\n results.push({\n spec,\n skipped: skip || (typeof expected === \"object\" && expected !== null && expected[SKIP_CLAUSE]),\n results: [{ pass: false, expected, received: \"\", op: spec, message: err }],\n subResults: [],\n });\n continue;\n }\n\n const jsonResults = runTest(spec, expected, received, skip);\n results.push(jsonResults);\n }\n\n return results;\n}\n\nfunction runTest(spec: string, expected: Assertion, received: any, skip?: boolean): SpecResult {\n // typeof null is also 'object'\n if (expected !== null && typeof expected === \"object\")\n return runObjectTests(expected, received, spec, skip);\n\n expected = getStringIfNotScalar(expected);\n received = getStringIfNotScalar(received);\n const pass = expected === received;\n\n return { spec, skipped: skip, results: [{ pass, expected, received, op: \":\" }], subResults: [] };\n}\n\nfunction getValueForJSONTests(responseContent: object, key: string, multi?: boolean): any {\n try {\n return multi ? jp.query(responseContent, key) : jp.value(responseContent, key);\n } catch (err: any) {\n throw new Error(`Error while evaluating JSONPath ${key}: ${err.description || err.message || err}`);\n }\n}\n\nfunction getType(data: any): string {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n return typeof data;\n}\n\nconst tests: {\n [name: string]: (\n expectedObj: any,\n receivedObj: any,\n spec: string,\n op: string,\n options: { [key: string]: any },\n ) => SpecResult;\n} = {\n $eq: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received === expected, expected, received, op }],\n };\n },\n $ne: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received !== expected, expected, received, op }],\n };\n },\n $gt: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received > expected, expected, received, op }],\n };\n },\n $lt: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received < expected, expected, received, op }],\n };\n },\n $lte: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received <= expected, expected, received, op }],\n };\n },\n $gte: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: received >= expected, expected, received, op }],\n };\n },\n $size: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const res: SpecResult = { spec, results: [], subResults: [] };\n\n const receivedLen: number | undefined =\n typeof receivedObj === \"object\" && receivedObj !== null\n ? Object.keys(receivedObj).length\n : typeof receivedObj === \"string\" || Array.isArray(receivedObj)\n ? receivedObj.length\n : undefined;\n\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj);\n const expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n\n if (typeof expectedObj === \"number\") {\n const compResult: TestResult = {\n pass: expected === receivedLen,\n op: op,\n expected: expected,\n received: `(length: ${receivedLen}) -> ${received}`,\n };\n res.results.push(compResult);\n\n return res;\n }\n\n if (isDict(expectedObj)) {\n // the spec remains the same, so we add it to the current layer\n const compRes: SpecResult = runObjectTests(expectedObj, receivedLen, spec);\n res.results.push(...compRes.results);\n res.subResults.push(...compRes.subResults);\n\n return res;\n }\n\n const compResult: TestResult = {\n pass: false,\n op: op,\n expected: expected,\n received: received,\n message: \"value for $size is not a number or valid JSON\",\n };\n res.results.push(compResult);\n\n return res;\n },\n $exists: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n const exists = received !== undefined;\n return {\n spec,\n subResults: [],\n results: [{ pass: exists === expected, expected, received, op }],\n };\n },\n $type: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const receivedType = getType(receivedObj);\n const receivedStr: string = `${getStringIfNotScalar(receivedObj)} (type ${receivedType})`;\n const expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [{ pass: expected === receivedType, expected, received: receivedStr, op }],\n };\n },\n $regex: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n\n const regexOpts = options[OPTIONS_CLAUSE];\n const regex = new RegExp(expected, regexOpts);\n let pass: boolean = false,\n message: string = \"\";\n try {\n pass = typeof received === \"string\" && regex.test(received);\n } catch (err: any) {\n message = err.message;\n }\n\n return {\n spec,\n subResults: [],\n results: [{ pass, expected, received, op, message }],\n };\n },\n $sw: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [\n { pass: typeof received === \"string\" && received.startsWith(expected), expected, received, op },\n ],\n };\n },\n $ew: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [\n { pass: typeof received === \"string\" && received.endsWith(expected), expected, received, op },\n ],\n };\n },\n $co: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const received: Exclude<any, object> = getStringIfNotScalar(receivedObj),\n expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n return {\n spec,\n subResults: [],\n results: [\n { pass: typeof received === \"string\" && received.includes(expected), expected, received, op },\n ],\n };\n },\n [OPTIONS_CLAUSE]: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n return {\n spec,\n subResults: [],\n results: [],\n };\n },\n [SKIP_CLAUSE]: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n return {\n spec,\n subResults: [],\n results: [],\n };\n },\n [MULTI_CLAUSE]: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n return {\n spec,\n subResults: [],\n results: [],\n };\n },\n $tests: function (expectedObj, receivedObj, spec, op, options): SpecResult {\n const res: SpecResult = { spec, results: [], subResults: [] };\n\n const expected: Exclude<any, object> = getStringIfNotScalar(expectedObj);\n