From f68fe8b7d3615cb514592170eab72a69c88219cf Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 10:02:11 +0900 Subject: [PATCH 1/8] Add JSONata check in http --- package-lock.json | 15 +++++++++++++++ package.json | 1 + src/index.ts | 4 ++++ src/steps/http.ts | 27 +++++++++++++++++++++++++++ tests/jsonata.yml | 14 ++++++++++++++ tests/test.ts | 1 + 6 files changed, 62 insertions(+) create mode 100644 tests/jsonata.yml 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..4bd0068 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 @@ -125,6 +127,7 @@ export type HTTPStepCheck = { json?: object schema?: object jsonpath?: StepCheckJSONPath | StepCheckMatcher + jsonata?: StepCheckJSONata | StepCheckMatcher xpath?: StepCheckValue | StepCheckMatcher selectors?: StepCheckValue | StepCheckMatcher cookies?: StepCheckValue | StepCheckMatcher @@ -550,6 +553,30 @@ export default async function ( } } + // Check JSONata + if (params.check.jsonata) { + stepResult.checks.jsonata = {} + try { + const json = JSON.parse(body) + for (const path in params.check.jsonata) { + const expression = jsonata(params.check.jsonata[path]) + stepResult.checks.jsonata[path] = { + expected: params.check.jsonata[path], + given: body, + passed: Boolean(await expression.evaluate(json)), + } + } + } catch { + for (const path in params.check.jsonata) { + stepResult.checks.jsonata[path] = { + expected: params.check.jsonata[path], + given: body, + passed: false, + } + } + } + } + // Check XPath if (params.check.xpath) { stepResult.checks.xpath = {} diff --git a/tests/jsonata.yml b/tests/jsonata.yml new file mode 100644 index 0000000..7fc745c --- /dev/null +++ b/tests/jsonata.yml @@ -0,0 +1,14 @@ +version: '1.1' +name: JSONata test +tests: + example: + steps: + - name: JSONATA evaluation test + http: + url: https://httpbin.org/ip + method: GET + check: + status: 200 + jsonata: + formatcheck: | + $match(origin, /(\d{1,3}\.){3}\d{1,3}/) 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)) From 1657049209dd0c1d6cc188fae140c01bf650a712 Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 11:16:20 +0900 Subject: [PATCH 2/8] - change given output from raw to - add a test --- src/steps/http.ts | 4 ++-- tests/jsonata.yml | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/steps/http.ts b/src/steps/http.ts index 4bd0068..894593f 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -562,7 +562,7 @@ export default async function ( const expression = jsonata(params.check.jsonata[path]) stepResult.checks.jsonata[path] = { expected: params.check.jsonata[path], - given: body, + given: '', passed: Boolean(await expression.evaluate(json)), } } @@ -570,7 +570,7 @@ export default async function ( for (const path in params.check.jsonata) { stepResult.checks.jsonata[path] = { expected: params.check.jsonata[path], - given: body, + given: '', passed: false, } } diff --git a/tests/jsonata.yml b/tests/jsonata.yml index 7fc745c..fb46dc8 100644 --- a/tests/jsonata.yml +++ b/tests/jsonata.yml @@ -12,3 +12,10 @@ tests: jsonata: formatcheck: | $match(origin, /(\d{1,3}\.){3}\d{1,3}/) + - http: + url: https://jsonplaceholder.typicode.com/posts/ + method: GET + check: + jsonata: + order: | + $[0].id > $[1].id From 15e56edb9a8757ee2a874de5ade93148e14f0782 Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 11:34:03 +0900 Subject: [PATCH 3/8] fix schema definition for jsonata --- src/steps/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steps/http.ts b/src/steps/http.ts index 894593f..426fe94 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -127,7 +127,7 @@ export type HTTPStepCheck = { json?: object schema?: object jsonpath?: StepCheckJSONPath | StepCheckMatcher - jsonata?: StepCheckJSONata | StepCheckMatcher + jsonata?: StepCheckJSONata xpath?: StepCheckValue | StepCheckMatcher selectors?: StepCheckValue | StepCheckMatcher cookies?: StepCheckValue | StepCheckMatcher From 34e286ed56348000ded0aeb63f91c85d2ee496de Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 13:06:54 +0900 Subject: [PATCH 4/8] improve return value of jsonata --- src/steps/http.ts | 12 +++++++----- tests/jsonata.yml | 13 +++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/steps/http.ts b/src/steps/http.ts index 426fe94..60bcdad 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -127,7 +127,7 @@ export type HTTPStepCheck = { json?: object schema?: object jsonpath?: StepCheckJSONPath | StepCheckMatcher - jsonata?: StepCheckJSONata + jsonata?: StepCheckJSONata | StepCheckMatcher xpath?: StepCheckValue | StepCheckMatcher selectors?: StepCheckValue | StepCheckMatcher cookies?: StepCheckValue | StepCheckMatcher @@ -559,11 +559,13 @@ export default async function ( try { const json = JSON.parse(body) for (const path in params.check.jsonata) { - const expression = jsonata(params.check.jsonata[path]) + const value = params.check.jsonata[path] + const expression = jsonata(value) + const result = await expression.evaluate(json) stepResult.checks.jsonata[path] = { - expected: params.check.jsonata[path], - given: '', - passed: Boolean(await expression.evaluate(json)), + expected: result.hasOwnProperty('expected') ? result.expected : value, + given: result.hasOwnProperty('given') ? result.given : json, + passed: Boolean(result.hasOwnProperty('passed') ? result.passed : result), } } } catch { diff --git a/tests/jsonata.yml b/tests/jsonata.yml index fb46dc8..b98d989 100644 --- a/tests/jsonata.yml +++ b/tests/jsonata.yml @@ -3,7 +3,7 @@ name: JSONata test tests: example: steps: - - name: JSONATA evaluation test + - name: JSONata evaluation test http: url: https://httpbin.org/ip method: GET @@ -12,10 +12,15 @@ tests: jsonata: formatcheck: | $match(origin, /(\d{1,3}\.){3}\d{1,3}/) - - http: + - name: JSONata return object sample + http: url: https://jsonplaceholder.typicode.com/posts/ method: GET check: jsonata: - order: | - $[0].id > $[1].id + notmatch: | + ( + $a := 99; + $b := $count($); + { "expected": $a, "given": $b, "passed": ($a = $b) } + ) From 60258547182f0dce6d5ab69e25690b299b9e238f Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 18:59:17 +0900 Subject: [PATCH 5/8] refactor --- src/steps/http.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/steps/http.ts b/src/steps/http.ts index 60bcdad..c68da97 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -127,7 +127,7 @@ export type HTTPStepCheck = { json?: object schema?: object jsonpath?: StepCheckJSONPath | StepCheckMatcher - jsonata?: StepCheckJSONata | StepCheckMatcher + jsonata?: StepCheckJSONata xpath?: StepCheckValue | StepCheckMatcher selectors?: StepCheckValue | StepCheckMatcher cookies?: StepCheckValue | StepCheckMatcher @@ -562,10 +562,11 @@ export default async function ( const value = params.check.jsonata[path] const expression = jsonata(value) const result = await expression.evaluate(json) + const { expected, given, passed } = result stepResult.checks.jsonata[path] = { - expected: result.hasOwnProperty('expected') ? result.expected : value, - given: result.hasOwnProperty('given') ? result.given : json, - passed: Boolean(result.hasOwnProperty('passed') ? result.passed : result), + expected: expected !== undefined ? expected : value, + given: given !== undefined ? given : json, + passed: passed !== undefined ? passed : !!result } } } catch { From f071807c1673f20c93e0207a07c823ce56f5c0da Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sat, 15 Feb 2025 19:00:45 +0900 Subject: [PATCH 6/8] add jsonata in captures --- src/steps/http.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/steps/http.ts b/src/steps/http.ts index c68da97..1aa5e34 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -110,6 +110,7 @@ export type HTTPStepCaptures = { export type HTTPStepCapture = { xpath?: string jsonpath?: string + jsonata?: string header?: string selector?: string cookie?: string @@ -450,6 +451,16 @@ export default async function ( } } + if (capture.jsonata) { + try { + const json = JSON.parse(body) + const expression = jsonata(capture.jsonata) + captures[name] = await expression.evaluate(json) + } catch { + captures[name] = undefined + } + } + if (capture.xpath) { const dom = new DOMParser().parseFromString(body) const result = xpath.select(capture.xpath, dom) From 5ad0103b8fef6de1bc888ab694d2ebb9200ec364 Mon Sep 17 00:00:00 2001 From: sakazuki Date: Sun, 16 Feb 2025 08:05:27 +0900 Subject: [PATCH 7/8] add captures.jsonata in tests --- tests/jsonata.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/jsonata.yml b/tests/jsonata.yml index b98d989..644d27f 100644 --- a/tests/jsonata.yml +++ b/tests/jsonata.yml @@ -7,6 +7,9 @@ tests: http: url: https://httpbin.org/ip method: GET + captures: + ip: + jsonata: origin check: status: 200 jsonata: From 59f85eb691682f50a7fe56484ba0e5130fd19236 Mon Sep 17 00:00:00 2001 From: sakazuki Date: Tue, 18 Feb 2025 08:18:29 +0900 Subject: [PATCH 8/8] add: jsonata check support the input definition using property name. --- src/steps/http.ts | 47 +++++++++++++++++++++++++++++++++-------------- tests/jsonata.ts | 9 +++++++++ tests/jsonata.yml | 5 +++-- 3 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 tests/jsonata.ts diff --git a/src/steps/http.ts b/src/steps/http.ts index 1aa5e34..2d83f9e 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -291,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) { @@ -311,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) @@ -445,7 +453,11 @@ 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 } @@ -567,17 +579,24 @@ 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 value = params.check.jsonata[path] - const expression = jsonata(value) - const result = await expression.evaluate(json) - const { expected, given, passed } = result + 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 : value, - given: given !== undefined ? given : json, - passed: passed !== undefined ? passed : !!result + expected: expected !== undefined ? expected : expression, + given: given !== undefined ? given : value, + passed: passed !== undefined ? passed : !!result, } } } catch { 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 index 644d27f..150ccf7 100644 --- a/tests/jsonata.yml +++ b/tests/jsonata.yml @@ -11,10 +11,11 @@ tests: ip: jsonata: origin check: - status: 200 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/ @@ -26,4 +27,4 @@ tests: $a := 99; $b := $count($); { "expected": $a, "given": $b, "passed": ($a = $b) } - ) + $count($[userId=1]): 10