diff --git a/package-lock.json b/package-lock.json index 35e8096..87e7ae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "form-data": "^4.0.0", "got": "^11.8.3", "js-yaml": "^4.1.0", + "jsonata": "^2.0.6", "jsonpath-plus": "^7.2.0", "liquidless": "^1.2.0", "liquidless-faker": "^1.0.1", @@ -1581,6 +1582,15 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/jsonata": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", + "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3549,6 +3559,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "jsonata": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", + "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", diff --git a/package.json b/package.json index 712c0aa..d4d0649 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "form-data": "^4.0.0", "got": "^11.8.3", "js-yaml": "^4.1.0", + "jsonata": "^2.0.6", "jsonpath-plus": "^7.2.0", "liquidless": "^1.2.0", "liquidless-faker": "^1.0.1", diff --git a/src/index.ts b/src/index.ts index 8952dcc..507a758 100644 --- a/src/index.ts +++ b/src/index.ts @@ -131,6 +131,10 @@ export type StepCheckJSONPath = { [key: string]: any } +export type StepCheckJSONata = { + [key: string]: any +} + export type StepCheckPerformance = { [key: string]: number } diff --git a/src/steps/http.ts b/src/steps/http.ts index fa55052..2d83f9e 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -25,6 +25,7 @@ import { import { StepCheckCaptures, StepCheckJSONPath, + StepCheckJSONata, StepCheckMatcher, StepCheckPerformance, StepCheckValue, @@ -33,6 +34,7 @@ import { WorkflowOptions, } from '..' import { Matcher, checkResult } from '../matcher' +import jsonata from 'jsonata' export type HTTPStepBase = { url: string @@ -108,6 +110,7 @@ export type HTTPStepCaptures = { export type HTTPStepCapture = { xpath?: string jsonpath?: string + jsonata?: string header?: string selector?: string cookie?: string @@ -125,6 +128,7 @@ export type HTTPStepCheck = { json?: object schema?: object jsonpath?: StepCheckJSONPath | StepCheckMatcher + jsonata?: StepCheckJSONata xpath?: StepCheckValue | StepCheckMatcher selectors?: StepCheckValue | StepCheckMatcher cookies?: StepCheckValue | StepCheckMatcher @@ -287,17 +291,17 @@ export default async function ( if (typeof params.formData[field] != 'object') { formData.append(field, params.formData[field]) } else if (Array.isArray(params.formData[field])) { - const stepFiles = params.formData[field] as StepFile[]; + const stepFiles = params.formData[field] as StepFile[] for (const stepFile of stepFiles) { const filepath = path.join( path.dirname(options?.path || __dirname), - stepFile.file, + stepFile.file ) - appendOptions.filename = path.parse(filepath).base; + appendOptions.filename = path.parse(filepath).base formData.append( field, await fs.promises.readFile(filepath), - appendOptions, + appendOptions ) } } else if ((params.formData[field] as StepFile).file) { @@ -307,12 +311,20 @@ export default async function ( stepFile.file ) appendOptions.filename = path.parse(filepath).base - formData.append(field, await fs.promises.readFile(filepath), appendOptions) + formData.append( + field, + await fs.promises.readFile(filepath), + appendOptions + ) } else { const requestPart = params.formData[field] as HTTPRequestPart if ('json' in requestPart) { appendOptions.contentType = 'application/json' - formData.append(field, JSON.stringify(requestPart.json), appendOptions) + formData.append( + field, + JSON.stringify(requestPart.json), + appendOptions + ) } else { appendOptions.contentType = requestPart.type formData.append(field, requestPart.value, appendOptions) @@ -441,7 +453,21 @@ export default async function ( if (capture.jsonpath) { try { const json = JSON.parse(body) - captures[name] = JSONPath({ path: capture.jsonpath, json, wrap: false }) + captures[name] = JSONPath({ + path: capture.jsonpath, + json, + wrap: false, + }) + } catch { + captures[name] = undefined + } + } + + if (capture.jsonata) { + try { + const json = JSON.parse(body) + const expression = jsonata(capture.jsonata) + captures[name] = await expression.evaluate(json) } catch { captures[name] = undefined } @@ -550,6 +576,40 @@ export default async function ( } } + // Check JSONata + if (params.check.jsonata) { + stepResult.checks.jsonata = {} + async function evalJSONata(expression: string, json: any) { + try { + return await jsonata(expression).evaluate(json) + } catch { + return json + } + } + try { + const json = JSON.parse(body) + for (const path in params.check.jsonata) { + const expression = params.check.jsonata[path] + const value = path.match(/^\$/) ? await evalJSONata(path, json) : json + const result = await evalJSONata(expression, value) + const { expected, given, passed } = result || {} + stepResult.checks.jsonata[path] = { + expected: expected !== undefined ? expected : expression, + given: given !== undefined ? given : value, + passed: passed !== undefined ? passed : !!result, + } + } + } catch { + for (const path in params.check.jsonata) { + stepResult.checks.jsonata[path] = { + expected: params.check.jsonata[path], + given: '', + passed: false, + } + } + } + } + // Check XPath if (params.check.xpath) { stepResult.checks.xpath = {} diff --git a/tests/jsonata.ts b/tests/jsonata.ts new file mode 100644 index 0000000..44091e4 --- /dev/null +++ b/tests/jsonata.ts @@ -0,0 +1,9 @@ +import { runFromFile } from '../src/index' + +runFromFile('./tests/jsonata.yml').then(({ result }) => { + for (const step of result.tests[0].steps) { + for (const key in step.checks) { + console.log(JSON.stringify(step.checks[key])) + } + } +}) diff --git a/tests/jsonata.yml b/tests/jsonata.yml new file mode 100644 index 0000000..150ccf7 --- /dev/null +++ b/tests/jsonata.yml @@ -0,0 +1,30 @@ +version: '1.1' +name: JSONata test +tests: + example: + steps: + - name: JSONata evaluation test + http: + url: https://httpbin.org/ip + method: GET + captures: + ip: + jsonata: origin + check: + jsonata: + formatcheck: | + $match(origin, /(\d{1,3}\.){3}\d{1,3}/) + # rewrite the above JSONata expression using the property name + $.origin: $match($, /(\d{1,3}\.){3}\d{1,3}/) + - name: JSONata return object sample + http: + url: https://jsonplaceholder.typicode.com/posts/ + method: GET + check: + jsonata: + notmatch: | + ( + $a := 99; + $b := $count($); + { "expected": $a, "given": $b, "passed": ($a = $b) } + $count($[userId=1]): 10 diff --git a/tests/test.ts b/tests/test.ts index 3713022..b959f64 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -5,3 +5,4 @@ const ee = new EventEmitter() runFromFile('./tests/basic.yml').then(({ result }) => console.log(result.tests[0].steps)) runFromFile('./tests/filelist.yml').then(({ result }) => console.log(result.tests[0].steps)) runFromFile('./tests/multipart.yml').then(({ result }) => console.log(result.tests[0].steps)) +runFromFile('./tests/jsonata.yml').then(({ result }) => console.log(result.tests[0].steps))