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