From ca89c2088e0710362364817af41dc4a1facdb6f3 Mon Sep 17 00:00:00 2001 From: "Matthew J. Martin" Date: Sun, 15 Mar 2026 04:24:10 +0700 Subject: [PATCH 1/2] fix: prevent matcher crash when jsonpath result is undefined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a JSONPath expression like `$.matchedCategories[*].name` evaluates against an empty array, the result is `undefined`. The `in`, `nin`, and `match` matchers called methods on `undefined` (e.g. `undefined.includes()`), throwing a TypeError. This error was caught by the catch-all block wrapping ALL jsonpath checks in http.ts, causing every jsonpath check in the step to be marked as failed with the raw body string — even checks that would have passed. Fixes: - matcher.ts: guard `in`, `nin`, and `match` against undefined/null `given` - http.ts: isolate try/catch per jsonpath key so one failure doesn't poison all other checks Co-Authored-By: Claude Opus 4.6 (1M context) --- src/matcher.ts | 6 +++--- src/steps/http.ts | 27 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/matcher.ts b/src/matcher.ts index 683aee2..face348 100644 --- a/src/matcher.ts +++ b/src/matcher.ts @@ -50,10 +50,10 @@ function check (given: any, expected: Matcher[] | any) : boolean { if ('lt' in test) return given < test.lt // @ts-ignore is possibly 'undefined' if ('lte' in test) return given <= test.lte - if ('in' in test) return given.includes(test.in) - if ('nin' in test) return !given.includes(test.nin) + if ('in' in test) return given != null && typeof given.includes === 'function' ? given.includes(test.in) : false + if ('nin' in test) return given != null && typeof given.includes === 'function' ? !given.includes(test.nin) : true // @ts-ignore is possibly 'undefined' - if ('match' in test) return new RegExp(test.match).test(given) + if ('match' in test) return given != null ? new RegExp(test.match).test(given) : false if ('isNumber' in test) return test.isNumber ? typeof given === 'number' : typeof given !== 'number' if ('isString' in test) return test.isString ? typeof given === 'string' : typeof given !== 'string' if ('isBoolean' in test) return test.isBoolean ? typeof given === 'boolean' : typeof given !== 'boolean' diff --git a/src/steps/http.ts b/src/steps/http.ts index d1ba7d7..6c267fe 100644 --- a/src/steps/http.ts +++ b/src/steps/http.ts @@ -530,15 +530,9 @@ export default async function ( // Check JSONPath if (params.check.jsonpath) { stepResult.checks.jsonpath = {} + let json: any try { - const json = JSON.parse(body) - for (const path in params.check.jsonpath) { - const result = JSONPath({ path, json, wrap: false }) - stepResult.checks.jsonpath[path] = checkResult( - result, - params.check.jsonpath[path] - ) - } + json = JSON.parse(body) } catch { for (const path in params.check.jsonpath) { stepResult.checks.jsonpath[path] = { @@ -548,6 +542,23 @@ export default async function ( } } } + if (json !== undefined) { + for (const path in params.check.jsonpath) { + try { + const result = JSONPath({ path, json, wrap: false }) + stepResult.checks.jsonpath[path] = checkResult( + result, + params.check.jsonpath[path] + ) + } catch { + stepResult.checks.jsonpath[path] = { + expected: params.check.jsonpath[path], + given: body, + passed: false, + } + } + } + } } // Check XPath From e8a93ef894c079b19ccd87ed7cdcfcf87f79e7cf Mon Sep 17 00:00:00 2001 From: "Matthew J. Martin" Date: Sun, 15 Mar 2026 05:39:53 +0700 Subject: [PATCH 2/2] build: compile dist for fix/matcher-undefined-crash --- dist/index.d.ts | 159 +++++++++++++ dist/index.js | 245 ++++++++++++++++++++ dist/loadtesting.d.ts | 57 +++++ dist/loadtesting.js | 111 +++++++++ dist/matcher.d.ts | 27 +++ dist/matcher.js | 65 ++++++ dist/steps/delay.d.ts | 2 + dist/steps/delay.js | 15 ++ dist/steps/graphql.d.ts | 7 + dist/steps/graphql.js | 16 ++ dist/steps/grpc.d.ts | 57 +++++ dist/steps/grpc.js | 120 ++++++++++ dist/steps/http.d.ts | 134 +++++++++++ dist/steps/http.js | 469 +++++++++++++++++++++++++++++++++++++++ dist/steps/plugin.d.ts | 10 + dist/steps/plugin.js | 7 + dist/steps/sse.d.ts | 39 ++++ dist/steps/sse.js | 131 +++++++++++ dist/steps/trpc.d.ts | 7 + dist/steps/trpc.js | 16 ++ dist/utils/auth.d.ts | 58 +++++ dist/utils/auth.js | 71 ++++++ dist/utils/files.d.ts | 8 + dist/utils/files.js | 17 ++ dist/utils/runner.d.ts | 12 + dist/utils/runner.js | 29 +++ dist/utils/schema.d.ts | 2 + dist/utils/schema.js | 9 + dist/utils/testdata.d.ts | 13 ++ dist/utils/testdata.js | 49 ++++ 30 files changed, 1962 insertions(+) create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/loadtesting.d.ts create mode 100644 dist/loadtesting.js create mode 100644 dist/matcher.d.ts create mode 100644 dist/matcher.js create mode 100644 dist/steps/delay.d.ts create mode 100644 dist/steps/delay.js create mode 100644 dist/steps/graphql.d.ts create mode 100644 dist/steps/graphql.js create mode 100644 dist/steps/grpc.d.ts create mode 100644 dist/steps/grpc.js create mode 100644 dist/steps/http.d.ts create mode 100644 dist/steps/http.js create mode 100644 dist/steps/plugin.d.ts create mode 100644 dist/steps/plugin.js create mode 100644 dist/steps/sse.d.ts create mode 100644 dist/steps/sse.js create mode 100644 dist/steps/trpc.d.ts create mode 100644 dist/steps/trpc.js create mode 100644 dist/utils/auth.d.ts create mode 100644 dist/utils/auth.js create mode 100644 dist/utils/files.d.ts create mode 100644 dist/utils/files.js create mode 100644 dist/utils/runner.d.ts create mode 100644 dist/utils/runner.js create mode 100644 dist/utils/schema.d.ts create mode 100644 dist/utils/schema.js create mode 100644 dist/utils/testdata.d.ts create mode 100644 dist/utils/testdata.js diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..f8a4359 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,159 @@ +/// +import { Cookie } from 'tough-cookie'; +import { EventEmitter } from 'node:events'; +import { Phase } from 'phasic'; +import { Matcher, CheckResult, CheckResults } from './matcher'; +import { LoadTestCheck } from './loadtesting'; +import { TestData } from './utils/testdata'; +import { CapturesStorage } from './utils/runner'; +import { CredentialsStorage } from './utils/auth'; +import { HTTPStep, HTTPStepRequest, HTTPStepResponse } from './steps/http'; +import { gRPCStep, gRPCStepRequest, gRPCStepResponse } from './steps/grpc'; +import { SSEStep, SSEStepRequest, SSEStepResponse } from './steps/sse'; +import { PluginStep } from './steps/plugin'; +import { tRPCStep } from './steps/trpc'; +import { GraphQLStep } from './steps/graphql'; +export declare type Workflow = { + version: string; + name: string; + env?: WorkflowEnv; + /** + * @deprecated Import files using `$refs` instead. + */ + include?: string[]; + before?: Test; + tests: Tests; + after?: Test; + components?: WorkflowComponents; + config?: WorkflowConfig; +}; +export declare type WorkflowEnv = { + [key: string]: string; +}; +export declare type WorkflowComponents = { + schemas?: { + [key: string]: any; + }; + credentials?: CredentialsStorage; +}; +export declare type WorkflowConfig = { + loadTest?: { + phases: Phase[]; + check?: LoadTestCheck; + }; + continueOnFail?: boolean; + http?: { + baseURL?: string; + rejectUnauthorized?: boolean; + http2?: boolean; + }; + grpc?: { + proto: string | string[]; + }; + concurrency?: number; +}; +export declare type WorkflowOptions = { + path?: string; + secrets?: WorkflowOptionsSecrets; + ee?: EventEmitter; + env?: WorkflowEnv; + concurrency?: number; +}; +declare type WorkflowOptionsSecrets = { + [key: string]: string; +}; +export declare type WorkflowResult = { + workflow: Workflow; + result: { + tests: TestResult[]; + passed: boolean; + timestamp: Date; + duration: number; + bytesSent: number; + bytesReceived: number; + co2: number; + }; + path?: string; +}; +export declare type Test = { + name?: string; + env?: object; + steps: Step[]; + testdata?: TestData; +}; +export declare type Tests = { + [key: string]: Test; +}; +export declare type Step = { + id?: string; + name?: string; + retries?: { + count: number; + interval?: string | number; + }; + if?: string; + http?: HTTPStep; + trpc?: tRPCStep; + graphql?: GraphQLStep; + grpc?: gRPCStep; + sse?: SSEStep; + delay?: string; + plugin?: PluginStep; +}; +export declare type StepCheckValue = { + [key: string]: string; +}; +export declare type StepCheckJSONPath = { + [key: string]: any; +}; +export declare type StepCheckPerformance = { + [key: string]: number; +}; +export declare type StepCheckCaptures = { + [key: string]: any; +}; +export declare type StepCheckMatcher = { + [key: string]: Matcher[]; +}; +export declare type TestResult = { + id: string; + name?: string; + steps: StepResult[]; + passed: boolean; + timestamp: Date; + duration: number; + co2: number; + bytesSent: number; + bytesReceived: number; +}; +export declare type StepResult = { + id?: string; + testId: string; + name?: string; + retries?: number; + captures?: CapturesStorage; + cookies?: Cookie.Serialized[]; + errored: boolean; + errorMessage?: string; + passed: boolean; + skipped: boolean; + timestamp: Date; + responseTime: number; + duration: number; + co2: number; + bytesSent: number; + bytesReceived: number; +} & StepRunResult; +export declare type StepRunResult = { + type?: string; + checks?: StepCheckResult; + request?: HTTPStepRequest | gRPCStepRequest | SSEStepRequest | any; + response?: HTTPStepResponse | gRPCStepResponse | SSEStepResponse | any; +}; +export declare type StepCheckResult = { + [key: string]: CheckResult | CheckResults; +}; +export declare function runFromYAML(yamlString: string, options?: WorkflowOptions): Promise; +export declare function runFromFile(path: string, options?: WorkflowOptions): Promise; +export declare function run(workflow: Workflow, options?: WorkflowOptions): Promise; +export {}; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..12e3961 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,245 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = exports.runFromFile = exports.runFromYAML = void 0; +const tough_cookie_1 = require("tough-cookie"); +const liquidless_1 = require("liquidless"); +const liquidless_faker_1 = require("liquidless-faker"); +const liquidless_naughtystrings_1 = require("liquidless-naughtystrings"); +const fs_1 = __importDefault(require("fs")); +const js_yaml_1 = __importDefault(require("js-yaml")); +const json_schema_ref_parser_1 = __importDefault(require("@apidevtools/json-schema-ref-parser")); +const ajv_1 = __importDefault(require("ajv")); +const ajv_formats_1 = __importDefault(require("ajv-formats")); +const p_limit_1 = __importDefault(require("p-limit")); +const node_path_1 = __importDefault(require("node:path")); +const testdata_1 = require("./utils/testdata"); +const runner_1 = require("./utils/runner"); +const http_1 = __importDefault(require("./steps/http")); +const grpc_1 = __importDefault(require("./steps/grpc")); +const sse_1 = __importDefault(require("./steps/sse")); +const delay_1 = __importDefault(require("./steps/delay")); +const plugin_1 = __importDefault(require("./steps/plugin")); +const trpc_1 = __importDefault(require("./steps/trpc")); +const graphql_1 = __importDefault(require("./steps/graphql")); +const parse_duration_1 = __importDefault(require("parse-duration")); +const schema_1 = require("./utils/schema"); +const templateDelimiters = ['${{', '}}']; +function renderObject(object, props) { + return (0, liquidless_1.renderObject)(object, props, { + filters: { + fake: liquidless_faker_1.fake, + naughtystring: liquidless_naughtystrings_1.naughtystring + }, + delimiters: templateDelimiters + }); +} +// Run from test file +async function runFromYAML(yamlString, options) { + const workflow = js_yaml_1.default.load(yamlString); + const dereffed = await json_schema_ref_parser_1.default.dereference(workflow, { + dereference: { + circular: 'ignore' + } + }); + return run(dereffed, options); +} +exports.runFromYAML = runFromYAML; +// Run from test file +async function runFromFile(path, options) { + const testFile = await fs_1.default.promises.readFile(path); + return runFromYAML(testFile.toString(), { ...options, path }); +} +exports.runFromFile = runFromFile; +// Run workflow +async function run(workflow, options) { + const timestamp = new Date(); + const schemaValidator = new ajv_1.default({ strictSchema: false }); + (0, ajv_formats_1.default)(schemaValidator); + // Templating for env, components, config + let env = { ...workflow.env, ...options?.env }; + if (workflow.env) { + env = renderObject(env, { env, secrets: options?.secrets }); + } + if (workflow.components) { + workflow.components = renderObject(workflow.components, { env, secrets: options?.secrets }); + } + if (workflow.components?.schemas) { + (0, schema_1.addCustomSchemas)(schemaValidator, workflow.components.schemas); + } + if (workflow.config) { + workflow.config = renderObject(workflow.config, { env, secrets: options?.secrets }); + } + if (workflow.include) { + for (const workflowPath of workflow.include) { + const testFile = await fs_1.default.promises.readFile(node_path_1.default.join(node_path_1.default.dirname(options?.path || __dirname), workflowPath)); + const test = js_yaml_1.default.load(testFile.toString()); + workflow.tests = { ...workflow.tests, ...test.tests }; + } + } + const concurrency = options?.concurrency || workflow.config?.concurrency || Object.keys(workflow.tests).length; + const limit = (0, p_limit_1.default)(concurrency <= 0 ? 1 : concurrency); + const testResults = []; + const captures = {}; + // Run `before` section + if (workflow.before) { + const beforeResult = await runTest('before', workflow.before, schemaValidator, options, workflow.config, env, captures); + testResults.push(beforeResult); + } + // Run `tests` section + const input = []; + Object.entries(workflow.tests).map(([id, test]) => input.push(limit(() => runTest(id, test, schemaValidator, options, workflow.config, env, { ...captures })))); + testResults.push(...await Promise.all(input)); + // Run `after` section + if (workflow.after) { + const afterResult = await runTest('after', workflow.after, schemaValidator, options, workflow.config, env, captures); + testResults.push(afterResult); + } + const workflowResult = { + workflow, + result: { + tests: testResults, + timestamp, + passed: testResults.every(test => test.passed), + duration: Date.now() - timestamp.valueOf(), + co2: testResults.map(test => test.co2).reduce((a, b) => a + b), + bytesSent: testResults.map(test => test.bytesSent).reduce((a, b) => a + b), + bytesReceived: testResults.map(test => test.bytesReceived).reduce((a, b) => a + b), + }, + path: options?.path + }; + options?.ee?.emit('workflow:result', workflowResult); + return workflowResult; +} +exports.run = run; +async function runTest(id, test, schemaValidator, options, config, env, capturesStorage) { + const testResult = { + id, + name: test.name, + steps: [], + passed: true, + timestamp: new Date(), + duration: 0, + co2: 0, + bytesSent: 0, + bytesReceived: 0 + }; + const captures = capturesStorage ?? {}; + const cookies = new tough_cookie_1.CookieJar(); + let previous; + let testData = {}; + // Load test data + if (test.testdata) { + const parsedCSV = await (0, testdata_1.parseCSV)(test.testdata, { ...test.testdata.options, workflowPath: options?.path }); + testData = parsedCSV[Math.floor(Math.random() * parsedCSV.length)]; + } + for (let step of test.steps) { + const tryStep = async () => runStep(previous, step, id, test, captures, cookies, schemaValidator, testData, options, config, env); + let stepResult = await tryStep(); + // Retries + if ((stepResult.errored || (!stepResult.passed && !stepResult.skipped)) && step.retries && step.retries.count > 0) { + for (let i = 0; i < step.retries.count; i++) { + await new Promise(resolve => { + if (typeof step.retries?.interval === 'string') { + setTimeout(resolve, (0, parse_duration_1.default)(step.retries?.interval) ?? undefined); + } + else { + setTimeout(resolve, step.retries?.interval); + } + }); + stepResult = await tryStep(); + if (stepResult.passed) + break; + } + } + testResult.steps.push(stepResult); + previous = stepResult; + options?.ee?.emit('step:result', stepResult); + } + testResult.duration = Date.now() - testResult.timestamp.valueOf(); + testResult.co2 = testResult.steps.map(step => step.co2).reduce((a, b) => a + b); + testResult.bytesSent = testResult.steps.map(step => step.bytesSent).reduce((a, b) => a + b); + testResult.bytesReceived = testResult.steps.map(step => step.bytesReceived).reduce((a, b) => a + b); + testResult.passed = testResult.steps.every(step => step.passed); + options?.ee?.emit('test:result', testResult); + return testResult; +} +async function runStep(previous, step, id, test, captures, cookies, schemaValidator, testData, options, config, env) { + let stepResult = { + id: step.id, + testId: id, + name: step.name, + timestamp: new Date(), + passed: true, + errored: false, + skipped: false, + duration: 0, + responseTime: 0, + bytesSent: 0, + bytesReceived: 0, + co2: 0 + }; + let runResult; + // Skip current step is the previous one failed or condition was unmet + if (!config?.continueOnFail && (previous && !previous.passed)) { + stepResult.passed = false; + stepResult.errorMessage = 'Step was skipped because previous one failed'; + stepResult.skipped = true; + } + else if (step.if && !(0, runner_1.checkCondition)(step.if, { captures, env: { ...env, ...test.env } })) { + stepResult.skipped = true; + stepResult.errorMessage = 'Step was skipped because the condition was unmet'; + } + else { + try { + step = renderObject(step, { + captures, + env: { ...env, ...test.env }, + secrets: options?.secrets, + testdata: testData + }); + if (step.http) { + runResult = await (0, http_1.default)(step.http, captures, cookies, schemaValidator, options, config); + } + if (step.trpc) { + runResult = await (0, trpc_1.default)(step.trpc, captures, cookies, schemaValidator, options, config); + } + if (step.graphql) { + runResult = await (0, graphql_1.default)(step.graphql, captures, cookies, schemaValidator, options, config); + } + if (step.grpc) { + runResult = await (0, grpc_1.default)(step.grpc, captures, schemaValidator, options, config); + } + if (step.sse) { + runResult = await (0, sse_1.default)(step.sse, captures, schemaValidator, options, config); + } + if (step.delay) { + runResult = await (0, delay_1.default)(step.delay); + } + if (step.plugin) { + runResult = await (0, plugin_1.default)(step.plugin, captures, cookies, schemaValidator, options, config); + } + stepResult.passed = (0, runner_1.didChecksPass)(runResult?.checks); + } + catch (error) { + stepResult.passed = false; + stepResult.errored = true; + stepResult.errorMessage = error.message; + options?.ee?.emit('step:error', error); + } + } + stepResult.type = runResult?.type; + stepResult.request = runResult?.request; + stepResult.response = runResult?.response; + stepResult.checks = runResult?.checks; + stepResult.responseTime = runResult?.response?.duration || 0; + stepResult.co2 = runResult?.response?.co2 || 0; + stepResult.bytesSent = runResult?.request?.size || 0; + stepResult.bytesReceived = runResult?.response?.size || 0; + stepResult.duration = Date.now() - stepResult.timestamp.valueOf(); + stepResult.captures = Object.keys(captures).length > 0 ? captures : undefined; + stepResult.cookies = Object.keys(cookies.toJSON().cookies).length > 0 ? cookies.toJSON().cookies : undefined; + return stepResult; +} diff --git a/dist/loadtesting.d.ts b/dist/loadtesting.d.ts new file mode 100644 index 0000000..372b77a --- /dev/null +++ b/dist/loadtesting.d.ts @@ -0,0 +1,57 @@ +import { Workflow, WorkflowOptions } from './index'; +import { Matcher, CheckResult } from './matcher'; +export declare type LoadTestResult = { + workflow: Workflow; + result: { + stats: { + tests: { + failed: number; + passed: number; + total: number; + }; + steps: { + failed: number; + passed: number; + skipped: number; + errored: number; + total: number; + }; + }; + bytesSent: number; + bytesReceived: number; + co2: number; + responseTime: LoadTestMetric; + iterations: number; + rps: number; + duration: number; + passed: boolean; + checks?: LoadTestChecksResult; + }; +}; +declare type LoadTestMetric = { + min: number; + max: number; + avg: number; + med: number; + p95: number; + p99: number; +}; +export declare type LoadTestCheck = { + min?: number | Matcher[]; + max?: number | Matcher[]; + avg?: number | Matcher[]; + med?: number | Matcher[]; + p95?: number | Matcher[]; + p99?: number | Matcher[]; +}; +declare type LoadTestChecksResult = { + min?: CheckResult; + max?: CheckResult; + avg?: CheckResult; + med?: CheckResult; + p95?: CheckResult; + p99?: CheckResult; +}; +export declare function loadTestFromFile(path: string, options?: WorkflowOptions): Promise; +export declare function loadTest(workflow: Workflow, options?: WorkflowOptions): Promise; +export {}; diff --git a/dist/loadtesting.js b/dist/loadtesting.js new file mode 100644 index 0000000..a19116b --- /dev/null +++ b/dist/loadtesting.js @@ -0,0 +1,111 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadTest = exports.loadTestFromFile = void 0; +const fs_1 = __importDefault(require("fs")); +const js_yaml_1 = __importDefault(require("js-yaml")); +const json_schema_ref_parser_1 = __importDefault(require("@apidevtools/json-schema-ref-parser")); +const phasic_1 = require("phasic"); +const simple_statistics_1 = require("simple-statistics"); +const index_1 = require("./index"); +const matcher_1 = require("./matcher"); +function metricsResult(numbers) { + return { + min: (0, simple_statistics_1.min)(numbers), + max: (0, simple_statistics_1.max)(numbers), + avg: (0, simple_statistics_1.mean)(numbers), + med: (0, simple_statistics_1.median)(numbers), + p95: (0, simple_statistics_1.quantile)(numbers, 0.95), + p99: (0, simple_statistics_1.quantile)(numbers, 0.99), + }; +} +async function loadTestFromFile(path, options) { + const testFile = await fs_1.default.promises.readFile(path); + const workflow = js_yaml_1.default.load(testFile.toString()); + const dereffed = await json_schema_ref_parser_1.default.dereference(workflow, { + dereference: { + circular: 'ignore' + } + }); + return loadTest(dereffed, { ...options, path }); +} +exports.loadTestFromFile = loadTestFromFile; +// Load-testing functionality +async function loadTest(workflow, options) { + if (!workflow.config?.loadTest?.phases) + throw Error('No load test config detected'); + const start = new Date(); + const resultList = await (0, phasic_1.runPhases)(workflow.config?.loadTest?.phases, () => (0, index_1.run)(workflow, options)); + const results = resultList.map(result => result.value.result); + // Tests metrics + const testsPassed = results.filter((r) => r.passed === true).length; + const testsFailed = results.filter((r) => r.passed === false).length; + // Steps metrics + const steps = results.map(r => r.tests).map(test => test.map(test => test.steps)).flat(2); + const stepsPassed = steps.filter(step => step.passed === true).length; + const stepsFailed = steps.filter(step => step.passed === false).length; + const stepsSkipped = steps.filter(step => step.skipped === true).length; + const stepsErrored = steps.filter(step => step.errored === true).length; + // Response metrics + const responseTime = metricsResult(steps.map(step => step.responseTime)); + // Size Metrics + const bytesSent = results.map(result => result.bytesSent).reduce((a, b) => a + b); + const bytesReceived = results.map(result => result.bytesReceived).reduce((a, b) => a + b); + const co2 = results.map(result => result.co2).reduce((a, b) => a + b); + // Checks + let checks; + if (workflow.config?.loadTest?.check) { + checks = {}; + if (workflow.config?.loadTest?.check.min) { + checks.min = (0, matcher_1.checkResult)(responseTime.min, workflow.config?.loadTest?.check.min); + } + if (workflow.config?.loadTest?.check.max) { + checks.max = (0, matcher_1.checkResult)(responseTime.max, workflow.config?.loadTest?.check.max); + } + if (workflow.config?.loadTest?.check.avg) { + checks.avg = (0, matcher_1.checkResult)(responseTime.avg, workflow.config?.loadTest?.check.avg); + } + if (workflow.config?.loadTest?.check.med) { + checks.med = (0, matcher_1.checkResult)(responseTime.med, workflow.config?.loadTest?.check.med); + } + if (workflow.config?.loadTest?.check.p95) { + checks.p95 = (0, matcher_1.checkResult)(responseTime.p95, workflow.config?.loadTest?.check.p95); + } + if (workflow.config?.loadTest?.check.p99) { + checks.p99 = (0, matcher_1.checkResult)(responseTime.p99, workflow.config?.loadTest?.check.p99); + } + } + const result = { + workflow, + result: { + stats: { + steps: { + failed: stepsFailed, + passed: stepsPassed, + skipped: stepsSkipped, + errored: stepsErrored, + total: steps.length + }, + tests: { + failed: testsFailed, + passed: testsPassed, + total: results.length + }, + }, + responseTime, + bytesSent, + bytesReceived, + co2, + rps: steps.length / ((Date.now() - start.valueOf()) / 1000), + iterations: results.length, + duration: Date.now() - start.valueOf(), + checks, + passed: checks ? Object.entries(checks).map(([i, check]) => check.passed).every(passed => passed) : true + } + }; + options?.ee?.emit('loadtest:result', result); + return result; +} +exports.loadTest = loadTest; diff --git a/dist/matcher.d.ts b/dist/matcher.d.ts new file mode 100644 index 0000000..4fe5276 --- /dev/null +++ b/dist/matcher.d.ts @@ -0,0 +1,27 @@ +export declare type Matcher = { + eq?: any; + ne?: any; + gt?: number; + gte?: number; + lt?: number; + lte?: number; + in?: object; + nin?: object; + match?: string; + isNumber?: boolean; + isString?: boolean; + isBoolean?: boolean; + isNull?: boolean; + isDefined?: boolean; + isObject?: boolean; + isArray?: boolean; +}; +export declare type CheckResult = { + expected: any; + given: any; + passed: boolean; +}; +export declare type CheckResults = { + [key: string]: CheckResult; +}; +export declare function checkResult(given: any, expected: Matcher[] | any): CheckResult; diff --git a/dist/matcher.js b/dist/matcher.js new file mode 100644 index 0000000..157de24 --- /dev/null +++ b/dist/matcher.js @@ -0,0 +1,65 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.checkResult = void 0; +const deep_equal_1 = __importDefault(require("deep-equal")); +function checkResult(given, expected) { + return { + expected, + given, + passed: check(given, expected) + }; +} +exports.checkResult = checkResult; +function check(given, expected) { + if (Array.isArray(expected)) { + return expected.map((test) => { + if ('eq' in test) + return (0, deep_equal_1.default)(given, test.eq, { strict: true }); + if ('ne' in test) + return given !== test.ne; + // @ts-ignore is possibly 'undefined' + if ('gt' in test) + return given > test.gt; + // @ts-ignore is possibly 'undefined' + if ('gte' in test) + return given >= test.gte; + // @ts-ignore is possibly 'undefined' + if ('lt' in test) + return given < test.lt; + // @ts-ignore is possibly 'undefined' + if ('lte' in test) + return given <= test.lte; + if ('in' in test) + return given != null && typeof given.includes === 'function' ? given.includes(test.in) : false; + if ('nin' in test) + return given != null && typeof given.includes === 'function' ? !given.includes(test.nin) : true; + // @ts-ignore is possibly 'undefined' + if ('match' in test) + return given != null ? new RegExp(test.match).test(given) : false; + if ('isNumber' in test) + return test.isNumber ? typeof given === 'number' : typeof given !== 'number'; + if ('isString' in test) + return test.isString ? typeof given === 'string' : typeof given !== 'string'; + if ('isBoolean' in test) + return test.isBoolean ? typeof given === 'boolean' : typeof given !== 'boolean'; + if ('isNull' in test) + return test.isNull ? given === null : given !== null; + if ('isDefined' in test) + return test.isDefined ? typeof given !== 'undefined' : typeof given === 'undefined'; + if ('isObject' in test) + return test.isObject ? typeof given === 'object' : typeof given !== 'object'; + if ('isArray' in test) + return test.isArray ? Array.isArray(given) : !Array.isArray(given); + }) + .every((test) => test === true); + } + // Check whether the expected value is regex + if (/^\/.*\/$/.test(expected)) { + const regex = new RegExp(expected.match(/^\/(.*?)\/$/)[1]); + return regex.test(given); + } + return (0, deep_equal_1.default)(given, expected); +} diff --git a/dist/steps/delay.d.ts b/dist/steps/delay.d.ts new file mode 100644 index 0000000..08edb17 --- /dev/null +++ b/dist/steps/delay.d.ts @@ -0,0 +1,2 @@ +import { StepRunResult } from '..'; +export default function (params: string | number): Promise; diff --git a/dist/steps/delay.js b/dist/steps/delay.js new file mode 100644 index 0000000..b9973ef --- /dev/null +++ b/dist/steps/delay.js @@ -0,0 +1,15 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const parse_duration_1 = __importDefault(require("parse-duration")); +async function default_1(params) { + const stepResult = { + type: 'delay', + }; + stepResult.type = 'delay'; + await new Promise((resolve) => setTimeout(resolve, typeof params === 'string' ? ((0, parse_duration_1.default)(params) ?? undefined) : params)); + return stepResult; +} +exports.default = default_1; diff --git a/dist/steps/graphql.d.ts b/dist/steps/graphql.d.ts new file mode 100644 index 0000000..ca44b60 --- /dev/null +++ b/dist/steps/graphql.d.ts @@ -0,0 +1,7 @@ +import Ajv from 'ajv'; +import { CookieJar } from 'tough-cookie'; +import { CapturesStorage } from '../utils/runner'; +import { WorkflowConfig, WorkflowOptions } from '..'; +import { HTTPStepBase, HTTPStepGraphQL } from './http'; +export declare type GraphQLStep = HTTPStepGraphQL & HTTPStepBase; +export default function (params: GraphQLStep, captures: CapturesStorage, cookies: CookieJar, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/graphql.js b/dist/steps/graphql.js new file mode 100644 index 0000000..29d4f7a --- /dev/null +++ b/dist/steps/graphql.js @@ -0,0 +1,16 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const http_1 = __importDefault(require("./http")); +async function default_1(params, captures, cookies, schemaValidator, options, config) { + return (0, http_1.default)({ + graphql: { + query: params.query, + variables: params.variables, + }, + ...params, + }, captures, cookies, schemaValidator, options, config); +} +exports.default = default_1; diff --git a/dist/steps/grpc.d.ts b/dist/steps/grpc.d.ts new file mode 100644 index 0000000..8c1c67a --- /dev/null +++ b/dist/steps/grpc.d.ts @@ -0,0 +1,57 @@ +import Ajv from 'ajv'; +import { gRPCRequestMetadata } from 'cool-grpc'; +import { StepCheckCaptures, StepCheckJSONPath, StepCheckMatcher, StepCheckPerformance } from '..'; +import { CapturesStorage } from './../utils/runner'; +import { Credential } from './../utils/auth'; +import { StepRunResult, WorkflowConfig, WorkflowOptions } from '..'; +import { Matcher } from '../matcher'; +export declare type gRPCStep = { + proto: string | string[]; + host: string; + service: string; + method: string; + data?: object | object[]; + timeout?: string | number; + metadata?: gRPCRequestMetadata; + auth?: gRPCStepAuth; + captures?: gRPCStepCaptures; + check?: gRPCStepCheck; +}; +export declare type gRPCStepAuth = { + tls?: Credential['tls']; +}; +export declare type gRPCStepCaptures = { + [key: string]: gRPCStepCapture; +}; +export declare type gRPCStepCapture = { + jsonpath?: string; +}; +export declare type gRPCStepCheck = { + json?: object; + schema?: object; + jsonpath?: StepCheckJSONPath | StepCheckMatcher; + captures?: StepCheckCaptures; + performance?: StepCheckPerformance | StepCheckMatcher; + size?: number | Matcher[]; + co2?: number | Matcher[]; +}; +export declare type gRPCStepRequest = { + proto?: string | string[]; + host: string; + service: string; + method: string; + metadata?: gRPCRequestMetadata; + data?: object | object[]; + tls?: Credential['tls']; + size?: number; +}; +export declare type gRPCStepResponse = { + body: object | object[]; + duration: number; + co2: number; + size: number; + status?: number; + statusText?: string; + metadata?: object; +}; +export default function (params: gRPCStep, captures: CapturesStorage, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/grpc.js b/dist/steps/grpc.js new file mode 100644 index 0000000..8c0d863 --- /dev/null +++ b/dist/steps/grpc.js @@ -0,0 +1,120 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_path_1 = __importDefault(require("node:path")); +const jsonpath_plus_1 = require("jsonpath-plus"); +const parse_duration_1 = __importDefault(require("parse-duration")); +const cool_grpc_1 = require("cool-grpc"); +const { co2 } = require('@tgwf/co2'); +const auth_1 = require("./../utils/auth"); +const matcher_1 = require("../matcher"); +async function default_1(params, captures, schemaValidator, options, config) { + const stepResult = { + type: 'grpc', + }; + const ssw = new co2(); + // Load TLS configuration from file or string + let tlsConfig; + if (params.auth) { + tlsConfig = await (0, auth_1.getTLSCertificate)(params.auth.tls, { + workflowPath: options?.path, + }); + } + const protos = []; + if (config?.grpc?.proto) { + protos.push(...config.grpc.proto); + } + if (params.proto) { + protos.push(...(Array.isArray(params.proto) ? params.proto : [params.proto])); + } + const proto = protos.map((p) => node_path_1.default.join(node_path_1.default.dirname(options?.path || __dirname), p)); + const request = { + proto, + host: params.host, + metadata: params.metadata, + service: params.service, + method: params.method, + data: params.data, + }; + const { metadata, statusCode, statusMessage, data, size } = await (0, cool_grpc_1.makeRequest)(proto, { + ...request, + tls: tlsConfig, + beforeRequest: (req) => { + options?.ee?.emit('step:grpc_request', request); + }, + afterResponse: (res) => { + options?.ee?.emit('step:grpc_response', res); + }, + options: { + deadline: typeof params.timeout === 'string' ? ((0, parse_duration_1.default)(params.timeout) ?? undefined) : params.timeout + } + }); + stepResult.request = request; + stepResult.response = { + body: data, + co2: ssw.perByte(size), + size: size, + status: statusCode, + statusText: statusMessage, + metadata, + }; + // Captures + if (params.captures) { + for (const name in params.captures) { + const capture = params.captures[name]; + if (capture.jsonpath) { + captures[name] = (0, jsonpath_plus_1.JSONPath)({ path: capture.jsonpath, json: data })[0]; + } + } + } + if (params.check) { + stepResult.checks = {}; + // Check JSON + if (params.check.json) { + stepResult.checks.json = (0, matcher_1.checkResult)(data, params.check.json); + } + // Check Schema + if (params.check.schema) { + const validate = schemaValidator.compile(params.check.schema); + stepResult.checks.schema = { + expected: params.check.schema, + given: data, + passed: validate(data), + }; + } + // Check JSONPath + if (params.check.jsonpath) { + stepResult.checks.jsonpath = {}; + for (const path in params.check.jsonpath) { + const result = (0, jsonpath_plus_1.JSONPath)({ path, json: data }); + stepResult.checks.jsonpath[path] = (0, matcher_1.checkResult)(result[0], params.check.jsonpath[path]); + } + } + // Check captures + if (params.check.captures) { + stepResult.checks.captures = {}; + for (const capture in params.check.captures) { + stepResult.checks.captures[capture] = (0, matcher_1.checkResult)(captures[capture], params.check.captures[capture]); + } + } + // Check performance + if (params.check.performance) { + stepResult.checks.performance = {}; + if (params.check.performance.total) { + stepResult.checks.performance.total = (0, matcher_1.checkResult)(stepResult.response?.duration, params.check.performance.total); + } + } + // Check byte size + if (params.check.size) { + stepResult.checks.size = (0, matcher_1.checkResult)(size, params.check.size); + } + // Check co2 emissions + if (params.check.co2) { + stepResult.checks.co2 = (0, matcher_1.checkResult)(stepResult.response?.co2, params.check.co2); + } + } + return stepResult; +} +exports.default = default_1; diff --git a/dist/steps/http.d.ts b/dist/steps/http.d.ts new file mode 100644 index 0000000..8f0084d --- /dev/null +++ b/dist/steps/http.d.ts @@ -0,0 +1,134 @@ +/// +import { Headers, PlainResponse } from 'got'; +import FormData from 'form-data'; +import Ajv from 'ajv'; +import { CookieJar } from 'tough-cookie'; +import { StepFile } from './../utils/files'; +import { CapturesStorage } from './../utils/runner'; +import { Credential } from './../utils/auth'; +import { StepCheckCaptures, StepCheckJSONPath, StepCheckMatcher, StepCheckPerformance, StepCheckValue, StepRunResult, WorkflowConfig, WorkflowOptions } from '..'; +import { Matcher } from '../matcher'; +export declare type HTTPStepBase = { + url: string; + method: string; + headers?: HTTPStepHeaders; + params?: HTTPStepParams; + cookies?: HTTPStepCookies; + auth?: Credential; + captures?: HTTPStepCaptures; + check?: HTTPStepCheck; + followRedirects?: boolean; + timeout?: string | number; + retries?: number; +}; +export declare type HTTPStep = { + body?: string | StepFile; + form?: HTTPStepForm; + formData?: HTTPStepMultiPartForm; + json?: object; + graphql?: HTTPStepGraphQL; + trpc?: HTTPStepTRPC; +} & HTTPStepBase; +export declare type HTTPStepTRPC = { + query?: { + [key: string]: object; + } | { + [key: string]: object; + }[]; + mutation?: { + [key: string]: object; + }; +}; +export declare type HTTPStepHeaders = { + [key: string]: string; +}; +export declare type HTTPStepParams = { + [key: string]: string; +}; +export declare type HTTPStepCookies = { + [key: string]: string; +}; +export declare type HTTPStepForm = { + [key: string]: string; +}; +export declare type HTTPRequestPart = { + type?: string; + value?: string; + json?: object; +}; +export declare type HTTPStepMultiPartForm = { + [key: string]: string | StepFile | HTTPRequestPart; +}; +export declare type HTTPStepGraphQL = { + query: string; + variables: object; +}; +export declare type HTTPStepCaptures = { + [key: string]: HTTPStepCapture; +}; +export declare type HTTPStepCapture = { + xpath?: string; + jsonpath?: string; + header?: string; + selector?: string; + cookie?: string; + regex?: string; + body?: boolean; +}; +export declare type HTTPStepCheck = { + status?: string | number | Matcher[]; + statusText?: string | Matcher[]; + redirected?: boolean; + redirects?: string[]; + headers?: StepCheckValue | StepCheckMatcher; + body?: string | Matcher[]; + json?: object; + schema?: object; + jsonpath?: StepCheckJSONPath | StepCheckMatcher; + xpath?: StepCheckValue | StepCheckMatcher; + selectors?: StepCheckValue | StepCheckMatcher; + cookies?: StepCheckValue | StepCheckMatcher; + captures?: StepCheckCaptures; + sha256?: string; + md5?: string; + performance?: StepCheckPerformance | StepCheckMatcher; + ssl?: StepCheckSSL; + size?: number | Matcher[]; + requestSize?: number | Matcher[]; + bodySize?: number | Matcher[]; + co2?: number | Matcher[]; +}; +export declare type StepCheckSSL = { + valid?: boolean; + signed?: boolean; + daysUntilExpiration?: number | Matcher[]; +}; +export declare type HTTPStepRequest = { + protocol: string; + url: string; + method?: string; + headers?: HTTPStepHeaders; + body?: string | Buffer | FormData; + size?: number; +}; +export declare type HTTPStepResponse = { + protocol: string; + status: number; + statusText?: string; + duration?: number; + contentType?: string; + timings: PlainResponse['timings']; + headers?: Headers; + ssl?: StepResponseSSL; + body: Buffer; + co2: number; + size?: number; + bodySize?: number; +}; +export declare type StepResponseSSL = { + valid: boolean; + signed: boolean; + validUntil: Date; + daysUntilExpiration: number; +}; +export default function (params: HTTPStep, captures: CapturesStorage, cookies: CookieJar, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/http.js b/dist/steps/http.js new file mode 100644 index 0000000..2a5ef42 --- /dev/null +++ b/dist/steps/http.js @@ -0,0 +1,469 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const got_1 = __importDefault(require("got")); +const parse_duration_1 = __importDefault(require("parse-duration")); +const proxy_agent_1 = require("proxy-agent"); +const xpath_1 = __importDefault(require("xpath")); +const cheerio = __importStar(require("cheerio")); +const xmldom_1 = require("@xmldom/xmldom"); +const jsonpath_plus_1 = require("jsonpath-plus"); +const { co2 } = require('@tgwf/co2'); +const form_data_1 = __importDefault(require("form-data")); +const fs_1 = __importDefault(require("fs")); +const node_crypto_1 = __importDefault(require("node:crypto")); +const node_https_1 = require("node:https"); +const node_path_1 = __importDefault(require("node:path")); +const files_1 = require("./../utils/files"); +const runner_1 = require("./../utils/runner"); +const auth_1 = require("./../utils/auth"); +const matcher_1 = require("../matcher"); +async function default_1(params, captures, cookies, schemaValidator, options, config) { + const stepResult = { + type: 'http', + }; + const ssw = new co2(); + let requestBody; + let url = params.url || ''; + // Prefix URL + if (config?.http?.baseURL) { + try { + new URL(url); + } + catch { + url = config.http.baseURL + params.url; + } + } + // Body + if (params.body) { + requestBody = await (0, files_1.tryFile)(params.body, { + workflowPath: options?.path, + }); + } + // JSON + if (params.json) { + if (!params.headers) + params.headers = {}; + if (!params.headers['Content-Type']) { + params.headers['Content-Type'] = 'application/json'; + } + requestBody = JSON.stringify(params.json); + } + // GraphQL + if (params.graphql) { + params.method = 'POST'; + if (!params.headers) + params.headers = {}; + params.headers['Content-Type'] = 'application/json'; + requestBody = JSON.stringify(params.graphql); + } + // tRPC + if (params.trpc) { + if (params.trpc.query) { + params.method = 'GET'; + // tRPC Batch queries + if (Array.isArray(params.trpc.query)) { + const payload = params.trpc.query.map((e) => { + return { + op: Object.keys(e)[0], + data: Object.values(e)[0], + }; + }); + const procedures = payload.map((p) => p.op).join(','); + url = url + '/' + procedures.replaceAll('/', '.'); + params.params = { + batch: '1', + input: JSON.stringify(Object.assign({}, payload.map((p) => p.data))), + }; + } + else { + const [procedure, data] = Object.entries(params.trpc.query)[0]; + url = url + '/' + procedure.replaceAll('/', '.'); + params.params = { + input: JSON.stringify(data), + }; + } + } + if (params.trpc.mutation) { + const [procedure, data] = Object.entries(params.trpc.mutation)[0]; + params.method = 'POST'; + url = url + '/' + procedure; + requestBody = JSON.stringify(data); + } + } + // Form Data + if (params.form) { + const formData = new URLSearchParams(); + for (const field in params.form) { + formData.append(field, params.form[field]); + } + requestBody = formData.toString(); + } + // Multipart Form Data + if (params.formData) { + const formData = new form_data_1.default(); + for (const field in params.formData) { + const appendOptions = {}; + if (typeof params.formData[field] != 'object') { + formData.append(field, params.formData[field]); + } + else if (Array.isArray(params.formData[field])) { + const stepFiles = params.formData[field]; + for (const stepFile of stepFiles) { + const filepath = node_path_1.default.join(node_path_1.default.dirname(options?.path || __dirname), stepFile.file); + appendOptions.filename = node_path_1.default.parse(filepath).base; + formData.append(field, await fs_1.default.promises.readFile(filepath), appendOptions); + } + } + else if (params.formData[field].file) { + const stepFile = params.formData[field]; + const filepath = node_path_1.default.join(node_path_1.default.dirname(options?.path || __dirname), stepFile.file); + appendOptions.filename = node_path_1.default.parse(filepath).base; + formData.append(field, await fs_1.default.promises.readFile(filepath), appendOptions); + } + else { + const requestPart = params.formData[field]; + if ('json' in requestPart) { + appendOptions.contentType = 'application/json'; + formData.append(field, JSON.stringify(requestPart.json), appendOptions); + } + else { + appendOptions.contentType = requestPart.type; + formData.append(field, requestPart.value, appendOptions); + } + } + } + requestBody = formData; + } + // Auth + let clientCredentials; + if (params.auth) { + const authHeader = await (0, auth_1.getAuthHeader)(params.auth); + if (authHeader) { + if (!params.headers) + params.headers = {}; + params.headers['Authorization'] = authHeader; + } + clientCredentials = await (0, auth_1.getClientCertificate)(params.auth.certificate, { + workflowPath: options?.path, + }); + } + // Set Cookies + if (params.cookies) { + for (const cookie in params.cookies) { + await cookies.setCookie(cookie + '=' + params.cookies[cookie], url); + } + } + let sslCertificate; + let requestSize = 0; + let responseSize = 0; + // Make a request + const res = await (0, got_1.default)(url, { + agent: { + http: new proxy_agent_1.ProxyAgent(), + https: new proxy_agent_1.ProxyAgent(new node_https_1.Agent({ maxCachedSessions: 0 })), + }, + method: params.method, + headers: { ...params.headers }, + body: requestBody, + searchParams: params.params + ? new URLSearchParams(params.params) + : undefined, + throwHttpErrors: false, + followRedirect: params.followRedirects ?? true, + timeout: typeof params.timeout === 'string' + ? ((0, parse_duration_1.default)(params.timeout) ?? undefined) + : params.timeout, + retry: params.retries ?? 0, + cookieJar: cookies, + http2: config?.http?.http2 ?? false, + https: { + ...clientCredentials, + rejectUnauthorized: config?.http?.rejectUnauthorized ?? false, + }, + }) + .on('request', (request) => options?.ee?.emit('step:http_request', request)) + .on('request', (request) => { + request.once('socket', (s) => { + s.once('close', () => { + requestSize = request.socket?.bytesWritten; + responseSize = request.socket?.bytesRead; + }); + }); + }) + .on('response', (response) => options?.ee?.emit('step:http_response', response)) + .on('response', (response) => { + if (response.socket.getPeerCertificate) { + sslCertificate = response.socket.getPeerCertificate(); + if (Object.keys(sslCertificate).length === 0) + sslCertificate = undefined; + } + }); + const responseData = res.rawBody; + const body = new TextDecoder().decode(responseData); + stepResult.request = { + protocol: 'HTTP/1.1', + url: res.url, + method: params.method, + headers: params.headers, + body: requestBody, + size: requestSize, + }; + stepResult.response = { + protocol: `HTTP/${res.httpVersion}`, + status: res.statusCode, + statusText: res.statusMessage, + duration: res.timings.phases.total, + headers: res.headers, + contentType: res.headers['content-type']?.split(';')[0], + timings: res.timings, + body: responseData, + size: responseSize, + bodySize: responseData.length, + co2: ssw.perByte(responseData.length), + }; + if (sslCertificate) { + stepResult.response.ssl = { + valid: new Date(sslCertificate.valid_to) > new Date(), + signed: sslCertificate.issuer.CN !== sslCertificate.subject.CN, + validUntil: new Date(sslCertificate.valid_to), + daysUntilExpiration: Math.round(Math.abs(new Date().valueOf() - new Date(sslCertificate.valid_to).valueOf()) / + (24 * 60 * 60 * 1000)), + }; + } + // Captures + if (params.captures) { + for (const name in params.captures) { + const capture = params.captures[name]; + if (capture.jsonpath) { + try { + const json = JSON.parse(body); + captures[name] = (0, jsonpath_plus_1.JSONPath)({ path: capture.jsonpath, json, wrap: false }); + } + catch { + captures[name] = undefined; + } + } + if (capture.xpath) { + const dom = new xmldom_1.DOMParser().parseFromString(body); + const result = xpath_1.default.select(capture.xpath, dom); + captures[name] = + result.length > 0 ? result[0].firstChild.data : undefined; + } + if (capture.header) { + captures[name] = res.headers[capture.header]; + } + if (capture.selector) { + const dom = cheerio.load(body); + captures[name] = dom(capture.selector).html(); + } + if (capture.cookie) { + captures[name] = (0, runner_1.getCookie)(cookies, capture.cookie, res.url); + } + if (capture.regex) { + captures[name] = body.match(capture.regex)?.[1]; + } + if (capture.body) { + captures[name] = body; + } + } + } + if (params.check) { + stepResult.checks = {}; + // Check headers + if (params.check.headers) { + stepResult.checks.headers = {}; + for (const header in params.check.headers) { + stepResult.checks.headers[header] = (0, matcher_1.checkResult)(res.headers[header.toLowerCase()], params.check.headers[header]); + } + } + // Check body + if (params.check.body) { + stepResult.checks.body = (0, matcher_1.checkResult)(body.trim(), params.check.body); + } + // Check JSON + if (params.check.json) { + try { + const json = JSON.parse(body); + stepResult.checks.json = (0, matcher_1.checkResult)(json, params.check.json); + } + catch { + stepResult.checks.json = { + expected: params.check.json, + given: body, + passed: false, + }; + } + } + // Check Schema + if (params.check.schema) { + let sample = body; + if (res.headers['content-type']?.includes('json')) { + sample = JSON.parse(body); + } + const validate = schemaValidator.compile(params.check.schema); + stepResult.checks.schema = { + expected: params.check.schema, + given: sample, + passed: validate(sample), + }; + } + // Check JSONPath + if (params.check.jsonpath) { + stepResult.checks.jsonpath = {}; + let json; + try { + json = JSON.parse(body); + } + catch { + for (const path in params.check.jsonpath) { + stepResult.checks.jsonpath[path] = { + expected: params.check.jsonpath[path], + given: body, + passed: false, + }; + } + } + if (json !== undefined) { + for (const path in params.check.jsonpath) { + try { + const result = (0, jsonpath_plus_1.JSONPath)({ path, json, wrap: false }); + stepResult.checks.jsonpath[path] = (0, matcher_1.checkResult)(result, params.check.jsonpath[path]); + } + catch { + stepResult.checks.jsonpath[path] = { + expected: params.check.jsonpath[path], + given: body, + passed: false, + }; + } + } + } + } + // Check XPath + if (params.check.xpath) { + stepResult.checks.xpath = {}; + for (const path in params.check.xpath) { + const dom = new xmldom_1.DOMParser().parseFromString(body); + const result = xpath_1.default.select(path, dom); + stepResult.checks.xpath[path] = (0, matcher_1.checkResult)(result.length > 0 ? result[0].firstChild.data : undefined, params.check.xpath[path]); + } + } + // Check HTML5 Selectors + if (params.check.selectors) { + stepResult.checks.selectors = {}; + const dom = cheerio.load(body); + for (const selector in params.check.selectors) { + const result = dom(selector).html(); + stepResult.checks.selectors[selector] = (0, matcher_1.checkResult)(result, params.check.selectors[selector]); + } + } + // Check Cookies + if (params.check.cookies) { + stepResult.checks.cookies = {}; + for (const cookie in params.check.cookies) { + const value = (0, runner_1.getCookie)(cookies, cookie, res.url); + stepResult.checks.cookies[cookie] = (0, matcher_1.checkResult)(value, params.check.cookies[cookie]); + } + } + // Check captures + if (params.check.captures) { + stepResult.checks.captures = {}; + for (const capture in params.check.captures) { + stepResult.checks.captures[capture] = (0, matcher_1.checkResult)(captures[capture], params.check.captures[capture]); + } + } + // Check status + if (params.check.status) { + stepResult.checks.status = (0, matcher_1.checkResult)(res.statusCode, params.check.status); + } + // Check statusText + if (params.check.statusText) { + stepResult.checks.statusText = (0, matcher_1.checkResult)(res.statusMessage, params.check.statusText); + } + // Check whether request was redirected + if ('redirected' in params.check) { + stepResult.checks.redirected = (0, matcher_1.checkResult)(res.redirectUrls.length > 0, params.check.redirected); + } + // Check redirects + if (params.check.redirects) { + stepResult.checks.redirects = (0, matcher_1.checkResult)(res.redirectUrls, params.check.redirects); + } + // Check sha256 + if (params.check.sha256) { + const hash = node_crypto_1.default + .createHash('sha256') + .update(Buffer.from(responseData)) + .digest('hex'); + stepResult.checks.sha256 = (0, matcher_1.checkResult)(hash, params.check.sha256); + } + // Check md5 + if (params.check.md5) { + const hash = node_crypto_1.default + .createHash('md5') + .update(Buffer.from(responseData)) + .digest('hex'); + stepResult.checks.md5 = (0, matcher_1.checkResult)(hash, params.check.md5); + } + // Check Performance + if (params.check.performance) { + stepResult.checks.performance = {}; + for (const metric in params.check.performance) { + stepResult.checks.performance[metric] = (0, matcher_1.checkResult)(res.timings.phases[metric], params.check.performance[metric]); + } + } + // Check SSL certs + if (params.check.ssl && sslCertificate) { + stepResult.checks.ssl = {}; + if ('valid' in params.check.ssl) { + stepResult.checks.ssl.valid = (0, matcher_1.checkResult)(stepResult.response?.ssl.valid, params.check.ssl.valid); + } + if ('signed' in params.check.ssl) { + stepResult.checks.ssl.signed = (0, matcher_1.checkResult)(stepResult.response?.ssl.signed, params.check.ssl.signed); + } + if (params.check.ssl.daysUntilExpiration) { + stepResult.checks.ssl.daysUntilExpiration = (0, matcher_1.checkResult)(stepResult.response?.ssl.daysUntilExpiration, params.check.ssl.daysUntilExpiration); + } + } + // Check request/response size + if (params.check.size) { + stepResult.checks.size = (0, matcher_1.checkResult)(responseSize, params.check.size); + } + if (params.check.requestSize) { + stepResult.checks.requestSize = (0, matcher_1.checkResult)(requestSize, params.check.requestSize); + } + if (params.check.bodySize) { + stepResult.checks.bodySize = (0, matcher_1.checkResult)(stepResult.response?.bodySize, params.check.bodySize); + } + if (params.check.co2) { + stepResult.checks.co2 = (0, matcher_1.checkResult)(stepResult.response.co2, params.check.co2); + } + } + return stepResult; +} +exports.default = default_1; diff --git a/dist/steps/plugin.d.ts b/dist/steps/plugin.d.ts new file mode 100644 index 0000000..3f0e13d --- /dev/null +++ b/dist/steps/plugin.d.ts @@ -0,0 +1,10 @@ +import Ajv from 'ajv'; +import { CookieJar } from 'tough-cookie'; +import { CapturesStorage } from '../utils/runner'; +import { WorkflowConfig, WorkflowOptions } from '..'; +export declare type PluginStep = { + id: string; + params?: object; + check?: object; +}; +export default function (params: PluginStep, captures: CapturesStorage, cookies: CookieJar, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/plugin.js b/dist/steps/plugin.js new file mode 100644 index 0000000..e60f686 --- /dev/null +++ b/dist/steps/plugin.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +async function default_1(params, captures, cookies, schemaValidator, options, config) { + const plugin = require(params.id); + return plugin.default(params, captures, cookies, schemaValidator, options, config); +} +exports.default = default_1; diff --git a/dist/steps/sse.d.ts b/dist/steps/sse.d.ts new file mode 100644 index 0000000..9d5075e --- /dev/null +++ b/dist/steps/sse.d.ts @@ -0,0 +1,39 @@ +/// +import { CapturesStorage } from './../utils/runner'; +import { Matcher } from '../matcher'; +import Ajv from 'ajv'; +import { StepCheckJSONPath, StepCheckMatcher, StepRunResult, WorkflowConfig, WorkflowOptions } from '..'; +import { Credential } from './../utils/auth'; +import { HTTPStepHeaders, HTTPStepParams } from './http'; +export declare type SSEStep = { + url: string; + headers?: HTTPStepHeaders; + params?: HTTPStepParams; + auth?: Credential; + json?: object; + check?: { + messages?: SSEStepCheck[]; + }; + timeout?: number; +}; +export declare type SSEStepCheck = { + id: string; + json?: object; + schema?: object; + jsonpath?: StepCheckJSONPath | StepCheckMatcher; + body?: string | Matcher[]; +}; +export declare type SSEStepRequest = { + url?: string; + headers?: HTTPStepHeaders; + size?: number; +}; +export declare type SSEStepResponse = { + contentType?: string; + duration?: number; + body: Buffer; + size?: number; + bodySize?: number; + co2: number; +}; +export default function (params: SSEStep, captures: CapturesStorage, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/sse.js b/dist/steps/sse.js new file mode 100644 index 0000000..9cf9c8f --- /dev/null +++ b/dist/steps/sse.js @@ -0,0 +1,131 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const eventsource_1 = __importDefault(require("eventsource")); +const jsonpath_plus_1 = require("jsonpath-plus"); +const { co2 } = require('@tgwf/co2'); +const matcher_1 = require("../matcher"); +const auth_1 = require("./../utils/auth"); +async function default_1(params, captures, schemaValidator, options, config) { + const stepResult = { + type: 'sse', + }; + const ssw = new co2(); + stepResult.type = 'sse'; + if (params.auth) { + const authHeader = await (0, auth_1.getAuthHeader)(params.auth); + if (authHeader) { + if (!params.headers) + params.headers = {}; + params.headers['Authorization'] = authHeader; + } + } + await new Promise((resolve, reject) => { + const ev = new eventsource_1.default(params.url || '', { + headers: params.headers, + rejectUnauthorized: config?.http?.rejectUnauthorized ?? false, + }); + const messages = []; + const timeout = setTimeout(() => { + ev.close(); + const messagesBuffer = Buffer.from(messages.map((m) => m.data).join('\n')); + stepResult.request = { + url: params.url, + headers: params.headers, + size: 0, + }; + stepResult.response = { + contentType: 'text/event-stream', + body: messagesBuffer, + size: messagesBuffer.length, + bodySize: messagesBuffer.length, + co2: ssw.perByte(messagesBuffer.length), + duration: params.timeout, + }; + resolve(true); + }, params.timeout || 10000); + ev.onerror = (error) => { + clearTimeout(timeout); + ev.close(); + reject(error); + }; + if (params.check) { + if (!stepResult.checks) + stepResult.checks = {}; + if (!stepResult.checks.messages) + stepResult.checks.messages = {}; + params.check.messages?.forEach((check) => { + ; + (stepResult.checks?.messages)[check.id] = { + expected: check.body || check.json || check.jsonpath || check.schema, + given: undefined, + passed: false, + }; + }); + } + ev.onmessage = (message) => { + messages.push(message); + if (params.check) { + params.check.messages?.forEach((check, id) => { + if (check.body) { + const result = (0, matcher_1.checkResult)(message.data, check.body); + if (result.passed && stepResult.checks?.messages) + stepResult.checks.messages[check.id] = result; + } + if (check.json) { + try { + const result = (0, matcher_1.checkResult)(JSON.parse(message.data), check.json); + if (result.passed && stepResult.checks?.messages) + stepResult.checks.messages[check.id] = result; + } + catch (e) { + reject(e); + } + } + if (check.schema) { + try { + const sample = JSON.parse(message.data); + const validate = schemaValidator.compile(check.schema); + const result = { + expected: check.schema, + given: sample, + passed: validate(sample), + }; + if (result.passed && stepResult.checks?.messages) + stepResult.checks.messages[check.id] = result; + } + catch (e) { + reject(e); + } + } + if (check.jsonpath) { + try { + let jsonpathResult = {}; + const json = JSON.parse(message.data); + for (const path in check.jsonpath) { + const result = (0, jsonpath_plus_1.JSONPath)({ path, json }); + jsonpathResult[path] = (0, matcher_1.checkResult)(result[0], check.jsonpath[path]); + } + const passed = Object.values(jsonpathResult) + .map((c) => c.passed) + .every((passed) => passed); + if (passed && stepResult.checks?.messages) + stepResult.checks.messages[check.id] = { + expected: check.jsonpath, + given: jsonpathResult, + passed, + }; + } + catch (e) { + reject(e); + } + } + }); + } + }; + }); + return stepResult; +} +exports.default = default_1; diff --git a/dist/steps/trpc.d.ts b/dist/steps/trpc.d.ts new file mode 100644 index 0000000..e18a687 --- /dev/null +++ b/dist/steps/trpc.d.ts @@ -0,0 +1,7 @@ +import Ajv from 'ajv'; +import { CookieJar } from 'tough-cookie'; +import { CapturesStorage } from '../utils/runner'; +import { WorkflowConfig, WorkflowOptions } from '..'; +import { HTTPStepBase, HTTPStepTRPC } from './http'; +export declare type tRPCStep = HTTPStepTRPC & HTTPStepBase; +export default function (params: tRPCStep, captures: CapturesStorage, cookies: CookieJar, schemaValidator: Ajv, options?: WorkflowOptions, config?: WorkflowConfig): Promise; diff --git a/dist/steps/trpc.js b/dist/steps/trpc.js new file mode 100644 index 0000000..b7ae911 --- /dev/null +++ b/dist/steps/trpc.js @@ -0,0 +1,16 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const http_1 = __importDefault(require("./http")); +async function default_1(params, captures, cookies, schemaValidator, options, config) { + return (0, http_1.default)({ + trpc: { + query: params.query, + mutation: params.mutation, + }, + ...params, + }, captures, cookies, schemaValidator, options, config); +} +exports.default = default_1; diff --git a/dist/utils/auth.d.ts b/dist/utils/auth.d.ts new file mode 100644 index 0000000..eaeb000 --- /dev/null +++ b/dist/utils/auth.d.ts @@ -0,0 +1,58 @@ +/// +import { StepFile, TryFileOptions } from './files'; +export declare type Credential = { + basic?: { + username: string; + password: string; + }; + bearer?: { + token: string; + }; + oauth?: { + endpoint: string; + client_id: string; + client_secret: string; + audience?: string; + }; + certificate?: { + ca?: string | StepFile; + cert?: string | StepFile; + key?: string | StepFile; + passphrase?: string; + }; + tls?: { + rootCerts?: string | StepFile; + privateKey?: string | StepFile; + certChain?: string | StepFile; + }; +}; +export declare type CredentialsStorage = { + [key: string]: Credential; +}; +declare type OAuthClientConfig = { + endpoint: string; + client_id: string; + client_secret: string; + audience?: string; +}; +export declare type OAuthResponse = { + access_token: string; + expires_in: number; + token_type: string; +}; +export declare type HTTPCertificate = { + certificate?: string | Buffer; + key?: string | Buffer; + certificateAuthority?: string | Buffer; + passphrase?: string; +}; +export declare type TLSCertificate = { + rootCerts?: string | Buffer; + privateKey?: string | Buffer; + certChain?: string | Buffer; +}; +export declare function getOAuthToken(clientConfig: OAuthClientConfig): Promise; +export declare function getAuthHeader(credential: Credential): Promise; +export declare function getClientCertificate(certificate: Credential['certificate'], options?: TryFileOptions): Promise; +export declare function getTLSCertificate(certificate: Credential['tls'], options?: TryFileOptions): Promise; +export {}; diff --git a/dist/utils/auth.js b/dist/utils/auth.js new file mode 100644 index 0000000..cb1d756 --- /dev/null +++ b/dist/utils/auth.js @@ -0,0 +1,71 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getTLSCertificate = exports.getClientCertificate = exports.getAuthHeader = exports.getOAuthToken = void 0; +const got_1 = __importDefault(require("got")); +const files_1 = require("./files"); +async function getOAuthToken(clientConfig) { + return await got_1.default.post(clientConfig.endpoint, { + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + grant_type: 'client_credentials', + client_id: clientConfig.client_id, + client_secret: clientConfig.client_secret, + audience: clientConfig.audience + }) + }) + .json(); +} +exports.getOAuthToken = getOAuthToken; +async function getAuthHeader(credential) { + if (credential.basic) { + return 'Basic ' + Buffer.from(credential.basic.username + ':' + credential.basic.password).toString('base64'); + } + if (credential.bearer) { + return 'Bearer ' + credential.bearer.token; + } + if (credential.oauth) { + const { access_token } = await getOAuthToken(credential.oauth); + return 'Bearer ' + access_token; + } +} +exports.getAuthHeader = getAuthHeader; +async function getClientCertificate(certificate, options) { + if (certificate) { + const cert = {}; + if (certificate.cert) { + cert.certificate = await (0, files_1.tryFile)(certificate.cert, options); + } + if (certificate.key) { + cert.key = await (0, files_1.tryFile)(certificate.key, options); + } + if (certificate.ca) { + cert.certificateAuthority = await (0, files_1.tryFile)(certificate.ca, options); + } + if (certificate.passphrase) { + cert.passphrase = certificate.passphrase; + } + return cert; + } +} +exports.getClientCertificate = getClientCertificate; +async function getTLSCertificate(certificate, options) { + if (certificate) { + const tlsConfig = {}; + if (certificate.rootCerts) { + tlsConfig.rootCerts = await (0, files_1.tryFile)(certificate.rootCerts, options); + } + if (certificate.privateKey) { + tlsConfig.privateKey = await (0, files_1.tryFile)(certificate.privateKey, options); + } + if (certificate.certChain) { + tlsConfig.certChain = await (0, files_1.tryFile)(certificate.certChain, options); + } + return tlsConfig; + } +} +exports.getTLSCertificate = getTLSCertificate; diff --git a/dist/utils/files.d.ts b/dist/utils/files.d.ts new file mode 100644 index 0000000..9c90adf --- /dev/null +++ b/dist/utils/files.d.ts @@ -0,0 +1,8 @@ +/// +export declare type StepFile = { + file: string; +}; +export declare type TryFileOptions = { + workflowPath?: string; +}; +export declare function tryFile(input: string | StepFile, options?: TryFileOptions): Promise; diff --git a/dist/utils/files.js b/dist/utils/files.js new file mode 100644 index 0000000..0177c9a --- /dev/null +++ b/dist/utils/files.js @@ -0,0 +1,17 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.tryFile = void 0; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +async function tryFile(input, options) { + if (input.file) { + return await fs_1.default.promises.readFile(path_1.default.join(path_1.default.dirname(options?.workflowPath || __dirname), input.file)); + } + else { + return input; + } +} +exports.tryFile = tryFile; diff --git a/dist/utils/runner.d.ts b/dist/utils/runner.d.ts new file mode 100644 index 0000000..2d60048 --- /dev/null +++ b/dist/utils/runner.d.ts @@ -0,0 +1,12 @@ +import { StepCheckResult } from '../index'; +import { CookieJar } from 'tough-cookie'; +export declare type CapturesStorage = { + [key: string]: any; +}; +export declare type TestConditions = { + captures?: CapturesStorage; + env?: object; +}; +export declare function checkCondition(expression: string, data: TestConditions): boolean; +export declare function getCookie(store: CookieJar, name: string, url: string): string; +export declare function didChecksPass(checks?: StepCheckResult): boolean; diff --git a/dist/utils/runner.js b/dist/utils/runner.js new file mode 100644 index 0000000..ec69c21 --- /dev/null +++ b/dist/utils/runner.js @@ -0,0 +1,29 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.didChecksPass = exports.getCookie = exports.checkCondition = void 0; +const filtrex_1 = require("filtrex"); +const flat_1 = __importDefault(require("flat")); +// Check if expression +function checkCondition(expression, data) { + const filter = (0, filtrex_1.compileExpression)(expression); + return filter((0, flat_1.default)(data)); +} +exports.checkCondition = checkCondition; +// Get cookie +function getCookie(store, name, url) { + return store.getCookiesSync(url).filter(cookie => cookie.key === name)[0]?.value; +} +exports.getCookie = getCookie; +// Did all checks pass? +function didChecksPass(checks) { + if (!checks) + return true; + return Object.values(checks).map(check => { + return check['passed'] ? check.passed : Object.values(check).map((c) => c.passed).every(passed => passed); + }) + .every(passed => passed); +} +exports.didChecksPass = didChecksPass; diff --git a/dist/utils/schema.d.ts b/dist/utils/schema.d.ts new file mode 100644 index 0000000..afb72c1 --- /dev/null +++ b/dist/utils/schema.d.ts @@ -0,0 +1,2 @@ +import Ajv from 'ajv'; +export declare function addCustomSchemas(schemaValidator: Ajv, schemas: any): void; diff --git a/dist/utils/schema.js b/dist/utils/schema.js new file mode 100644 index 0000000..e85d29b --- /dev/null +++ b/dist/utils/schema.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addCustomSchemas = void 0; +function addCustomSchemas(schemaValidator, schemas) { + for (const schema in schemas) { + schemaValidator.addSchema(schemas[schema], `#/components/schemas/${schema}`); + } +} +exports.addCustomSchemas = addCustomSchemas; diff --git a/dist/utils/testdata.d.ts b/dist/utils/testdata.d.ts new file mode 100644 index 0000000..ed76a5d --- /dev/null +++ b/dist/utils/testdata.d.ts @@ -0,0 +1,13 @@ +export declare type TestData = { + content?: string; + file?: string; + options?: TestDataOptions; +}; +export declare type TestDataOptions = { + delimiter?: string; + quote?: string | null; + escape?: string; + headers?: boolean | string[]; + workflowPath?: string; +}; +export declare function parseCSV(testData: TestData, options?: TestDataOptions): Promise; diff --git a/dist/utils/testdata.js b/dist/utils/testdata.js new file mode 100644 index 0000000..10920dd --- /dev/null +++ b/dist/utils/testdata.js @@ -0,0 +1,49 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.parseCSV = void 0; +const csv = __importStar(require("@fast-csv/parse")); +const path_1 = __importDefault(require("path")); +// Parse CSV +function parseCSV(testData, options) { + return new Promise((resolve, reject) => { + const defaultOptions = { headers: true }; + let parsedData = []; + if (testData.file) { + csv.parseFile(path_1.default.join(path_1.default.dirname(options?.workflowPath || __dirname), testData.file), { ...defaultOptions, ...options }) + .on('data', data => parsedData.push(data)) + .on('end', () => resolve(parsedData)); + } + else { + csv.parseString(testData.content, { ...defaultOptions, ...options }) + .on('data', data => parsedData.push(data)) + .on('end', () => resolve(parsedData)); + } + }); +} +exports.parseCSV = parseCSV;