k6-cucumber-steps 1.2.18 โ 1.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/k6-cucumber-steps.js
CHANGED
|
@@ -24,11 +24,9 @@ program
|
|
|
24
24
|
.option("--overwrite", "Overwrite report files", false)
|
|
25
25
|
.option("--cleanReports", "Clean the reports folder before running", false)
|
|
26
26
|
.option("--reporter", "Enable report generation", false)
|
|
27
|
-
.option("--clean", "Alias for --cleanReports")
|
|
27
|
+
.option("--clean", "Alias for --cleanReports", false)
|
|
28
28
|
.option("-p, --payloadPath <dir>", "Directory for payload files")
|
|
29
29
|
// Add CLI options for all config options
|
|
30
|
-
.option("--script <file>", "k6 script file to run")
|
|
31
|
-
.option("--k6Config <file>", "k6 config file to use")
|
|
32
30
|
.action(async (argv) => {
|
|
33
31
|
// Load config file
|
|
34
32
|
const configFileInput =
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module.exports = function buildK6Script(config) {
|
|
2
|
-
const { method, endpoints, endpoint, body, headers, options, baseUrl } =
|
|
2
|
+
const { method, endpoints, endpoint, body, headers, options, baseUrl } =
|
|
3
|
+
config;
|
|
3
4
|
|
|
4
5
|
const BASE_URL =
|
|
5
6
|
baseUrl ||
|
|
@@ -54,7 +55,8 @@ export default function () {
|
|
|
54
55
|
: "JSON.stringify(body)"
|
|
55
56
|
}, { headers });
|
|
56
57
|
check(res${i}, {
|
|
57
|
-
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
58
|
+
"status is 2xx": (r) => r.status >= 200 && r.status < 300,
|
|
59
|
+
"response body is not empty": (r) => r.body && r.body.length > 0,
|
|
58
60
|
});
|
|
59
61
|
`
|
|
60
62
|
)
|
|
@@ -67,7 +69,8 @@ export default function () {
|
|
|
67
69
|
: "JSON.stringify(body)"
|
|
68
70
|
}, { headers });
|
|
69
71
|
check(res, {
|
|
70
|
-
"status is 2xx": (r) => r.status >= 200 && r.status < 300
|
|
72
|
+
"status is 2xx": (r) => r.status >= 200 && r.status < 300,
|
|
73
|
+
"response body is not empty": (r) => r.body && r.body.length > 0,
|
|
71
74
|
});
|
|
72
75
|
`
|
|
73
76
|
}
|
|
@@ -1,62 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module resolveBody
|
|
3
|
-
* @description
|
|
4
|
-
* This module resolves placeholders in a template string using environment variables,
|
|
5
|
-
* Faker templates, and JSON file includes.
|
|
6
|
-
* It supports the following types of placeholders:
|
|
7
|
-
* 1. Environment variables: {{username}} will be replaced with the value of process.env.username.
|
|
8
|
-
* 2. Faker templates: {{faker.internet.email}} will be replaced with a randomly generated email address.
|
|
9
|
-
* 3. JSON file includes: <address.json> will be replaced with the contents of the address.json file.
|
|
10
|
-
* */
|
|
11
1
|
const fs = require("fs");
|
|
12
2
|
const path = require("path");
|
|
13
|
-
const faker = require("@faker-js/faker");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new Error(`Invalid Faker
|
|
3
|
+
const faker = require("@faker-js/faker").faker;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively resolves all placeholders in an object.
|
|
7
|
+
* @param {*} data - The parsed JSON (object, array, string, etc)
|
|
8
|
+
* @param {object} env - Environment variables + aliases
|
|
9
|
+
* @returns {*} - Fully resolved structure
|
|
10
|
+
*/
|
|
11
|
+
function resolveDeep(data, env) {
|
|
12
|
+
if (typeof data === "string") {
|
|
13
|
+
// 1. Resolve env vars like {{username}}
|
|
14
|
+
data = data.replace(/{{(\w+)}}/g, (_, key) => {
|
|
15
|
+
return env[key] || "";
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 2. Resolve faker like {{faker.internet.email}}
|
|
19
|
+
data = data.replace(/{{faker\.([\w.]+)}}/g, (_, methodPath) => {
|
|
20
|
+
const parts = methodPath.split(".");
|
|
21
|
+
let fn = faker;
|
|
22
|
+
for (const part of parts) {
|
|
23
|
+
fn = fn?.[part];
|
|
24
|
+
if (!fn) throw new Error(`Invalid Faker method: faker.${methodPath}`);
|
|
35
25
|
}
|
|
26
|
+
return typeof fn === "function" ? fn() : fn;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 3. Replace JSON file includes like <address.json>
|
|
30
|
+
data = data.replace(/<([\w-]+\.json)>/g, (_, fileName) => {
|
|
31
|
+
const filePath = path.join(__dirname, "../payloads", fileName);
|
|
32
|
+
if (!fs.existsSync(filePath)) {
|
|
33
|
+
throw new Error(`Payload file not found: ${fileName}`);
|
|
34
|
+
}
|
|
35
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
36
|
+
return fileContent.trim();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(data)) {
|
|
43
|
+
return data.map((item) => resolveDeep(item, env));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof data === "object" && data !== null) {
|
|
47
|
+
const resolved = {};
|
|
48
|
+
for (const key in data) {
|
|
49
|
+
resolved[key] = resolveDeep(data[key], env);
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return fn();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Replace JSON file includes like <address.json>
|
|
44
|
-
result = result.replace(/<([\w.]+\.json)>/g, (_, fileName) => {
|
|
45
|
-
const filePath = path.join(__dirname, "../payloads", fileName);
|
|
46
|
-
if (!fs.existsSync(filePath)) {
|
|
47
|
-
throw new Error(`Payload file not found: ${fileName}`);
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
return JSON.stringify(JSON.parse(fs.readFileSync(filePath, "utf-8")));
|
|
51
|
-
} catch (error) {
|
|
52
|
-
throw new Error(`Failed to parse JSON file: ${fileName}`);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
55
53
|
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves JSON templates using env vars, Faker, and JSON includes.
|
|
59
|
+
* @param {string} template - The raw JSON string template
|
|
60
|
+
* @param {object} env - Object containing environment variables or aliases
|
|
61
|
+
* @returns {object} - Fully resolved JS object
|
|
62
|
+
*/
|
|
63
|
+
module.exports = function resolveBody(template, env = {}) {
|
|
56
64
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const parsed = JSON.parse(template);
|
|
66
|
+
return resolveDeep(parsed, env);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(
|
|
69
|
+
"โ Failed to parse input JSON before resolving:",
|
|
70
|
+
err.message
|
|
71
|
+
);
|
|
72
|
+
throw new Error("Invalid JSON template provided to resolveBody");
|
|
61
73
|
}
|
|
62
74
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { generateK6Script, runK6Script } = require("./runner");
|
|
2
|
+
const buildK6Script = require("./buildK6Script");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs a K6 script using values from the World instance.
|
|
6
|
+
* Supports both single and multiple endpoints.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} world - Cucumber World instance
|
|
9
|
+
* @param {string} method - HTTP method
|
|
10
|
+
* @param {boolean} isMulti - Flag to use `world.endpoints` instead of `endpoint`
|
|
11
|
+
*/
|
|
12
|
+
async function runK6ScriptFromWorld(world, method, isMulti = false) {
|
|
13
|
+
const scriptContent = buildK6Script({
|
|
14
|
+
method,
|
|
15
|
+
endpoint: isMulti ? undefined : world.endpoint,
|
|
16
|
+
endpoints: isMulti ? world.endpoints : undefined,
|
|
17
|
+
body: world.body,
|
|
18
|
+
headers: world.headers,
|
|
19
|
+
options: world.k6Options,
|
|
20
|
+
baseUrl: world.baseUrl,
|
|
21
|
+
worldParameters: world.worldParameters,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const scriptPath = await generateK6Script(scriptContent, "api", true);
|
|
25
|
+
const { stdout, stderr, code } = await runK6Script(scriptPath, true);
|
|
26
|
+
|
|
27
|
+
console.log(stdout);
|
|
28
|
+
if (stderr) console.error(stderr);
|
|
29
|
+
if (code !== 0) {
|
|
30
|
+
throw new Error("โ K6 script execution failed.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log("โ
K6 script ran successfully.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = runK6ScriptFromWorld;
|
package/package.json
CHANGED
|
@@ -7,9 +7,10 @@ import crypto from "crypto";
|
|
|
7
7
|
import * as dotenv from "dotenv";
|
|
8
8
|
import resolvePayloadPath from "../lib/helpers/resolvePayloadPath.js";
|
|
9
9
|
import resolveBody from "../lib/helpers/resolveBody.js";
|
|
10
|
-
import buildK6Script from "../lib/helpers/buildK6Script.js";
|
|
10
|
+
// import buildK6Script from "../lib/helpers/buildK6Script.js";
|
|
11
11
|
import generateHeaders from "../lib/helpers/generateHeaders.js";
|
|
12
|
-
import { runK6Script } from "../lib/utils/k6Runner.js";
|
|
12
|
+
// import { runK6Script } from "../lib/utils/k6Runner.js";
|
|
13
|
+
const runK6ScriptFromWorld = require("../../../helpers/runK6ScriptFromWorld");
|
|
13
14
|
|
|
14
15
|
dotenv.config();
|
|
15
16
|
|
|
@@ -608,79 +609,95 @@ if (!fs.existsSync(reportDir)) {
|
|
|
608
609
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
609
610
|
}
|
|
610
611
|
|
|
612
|
+
// Then(
|
|
613
|
+
// /^I see the API should handle the (\w+) request successfully$/,
|
|
614
|
+
// { timeout: 300000 },
|
|
615
|
+
// async function (method) {
|
|
616
|
+
// if (!this.config || !this.config.method) {
|
|
617
|
+
// throw new Error("Configuration is missing or incomplete.");
|
|
618
|
+
// }
|
|
619
|
+
// const expectedMethod = method.toUpperCase();
|
|
620
|
+
// const actualMethod = this.config.method.toUpperCase();
|
|
621
|
+
// if (actualMethod !== expectedMethod) {
|
|
622
|
+
// throw new Error(
|
|
623
|
+
// `Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
|
|
624
|
+
// );
|
|
625
|
+
// }
|
|
626
|
+
// try {
|
|
627
|
+
// const scriptContent = buildK6Script(this.config);
|
|
628
|
+
// const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
629
|
+
// const scriptFileName = `k6-script-${uniqueId}.js`;
|
|
630
|
+
// const scriptPath = path.join(reportDir, scriptFileName);
|
|
631
|
+
// fs.writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
632
|
+
// this.log?.(`โ
k6 script generated at: "${scriptPath}"`);
|
|
633
|
+
|
|
634
|
+
// this.log?.(`๐ Running k6 script: "${scriptFileName}"...`);
|
|
635
|
+
// const { stdout, stderr, code } = await runK6Script(
|
|
636
|
+
// scriptPath,
|
|
637
|
+
// process.env.K6_CUCUMBER_OVERWRITE === "true"
|
|
638
|
+
// );
|
|
639
|
+
// if (stdout) this.log?.(`k6 STDOUT:\n${stdout}`);
|
|
640
|
+
// if (stderr) this.log?.(`k6 STDERR:\n${stderr}`);
|
|
641
|
+
|
|
642
|
+
// if (code !== 0) {
|
|
643
|
+
// throw new Error(
|
|
644
|
+
// `k6 process exited with code ${code}. Check k6 output for details.`
|
|
645
|
+
// );
|
|
646
|
+
// }
|
|
647
|
+
// this.log?.(
|
|
648
|
+
// `โ
k6 script executed successfully for ${expectedMethod} request.`
|
|
649
|
+
// );
|
|
650
|
+
|
|
651
|
+
// const saveK6Script =
|
|
652
|
+
// process.env.saveK6Script === "true" ||
|
|
653
|
+
// process.env.SAVE_K6_SCRIPT === "true" ||
|
|
654
|
+
// this.parameters?.saveK6Script === true;
|
|
655
|
+
|
|
656
|
+
// if (!saveK6Script) {
|
|
657
|
+
// try {
|
|
658
|
+
// fs.unlinkSync(scriptPath);
|
|
659
|
+
// this.log?.(`๐งน Temporary k6 script deleted: "${scriptPath}"`);
|
|
660
|
+
// } catch (cleanupErr) {
|
|
661
|
+
// this.log?.(
|
|
662
|
+
// `โ ๏ธ Warning: Could not delete temporary k6 script file: "${scriptPath}". Error: ${
|
|
663
|
+
// cleanupErr instanceof Error
|
|
664
|
+
// ? cleanupErr.message
|
|
665
|
+
// : String(cleanupErr)
|
|
666
|
+
// }`
|
|
667
|
+
// );
|
|
668
|
+
// }
|
|
669
|
+
// } else {
|
|
670
|
+
// this.log?.(
|
|
671
|
+
// `โน๏ธ k6 script kept at: "${scriptPath}". Set SAVE_K6_SCRIPT=false to delete automatically.`
|
|
672
|
+
// );
|
|
673
|
+
// }
|
|
674
|
+
// } catch (error) {
|
|
675
|
+
// this.log?.(
|
|
676
|
+
// `โ Failed to generate or run k6 script: ${
|
|
677
|
+
// error instanceof Error ? error.message : String(error)
|
|
678
|
+
// }`
|
|
679
|
+
// );
|
|
680
|
+
// throw new Error(
|
|
681
|
+
// `k6 script generation or execution failed: ${
|
|
682
|
+
// error instanceof Error ? error.message : String(error)
|
|
683
|
+
// }`
|
|
684
|
+
// );
|
|
685
|
+
// }
|
|
686
|
+
// }
|
|
687
|
+
// );
|
|
688
|
+
|
|
689
|
+
// Single endpoint
|
|
611
690
|
Then(
|
|
612
|
-
|
|
613
|
-
{ timeout: 300000 },
|
|
691
|
+
"I see the API should handle the {word} request successfully",
|
|
614
692
|
async function (method) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const expectedMethod = method.toUpperCase();
|
|
619
|
-
const actualMethod = this.config.method.toUpperCase();
|
|
620
|
-
if (actualMethod !== expectedMethod) {
|
|
621
|
-
throw new Error(
|
|
622
|
-
`Mismatched HTTP method: expected "${expectedMethod}", got "${actualMethod}"`
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
try {
|
|
626
|
-
const scriptContent = buildK6Script(this.config);
|
|
627
|
-
const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
628
|
-
const scriptFileName = `k6-script-${uniqueId}.js`;
|
|
629
|
-
const scriptPath = path.join(reportDir, scriptFileName);
|
|
630
|
-
fs.writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
631
|
-
this.log?.(`โ
k6 script generated at: "${scriptPath}"`);
|
|
632
|
-
|
|
633
|
-
this.log?.(`๐ Running k6 script: "${scriptFileName}"...`);
|
|
634
|
-
const { stdout, stderr, code } = await runK6Script(
|
|
635
|
-
scriptPath,
|
|
636
|
-
process.env.K6_CUCUMBER_OVERWRITE === "true"
|
|
637
|
-
);
|
|
638
|
-
if (stdout) this.log?.(`k6 STDOUT:\n${stdout}`);
|
|
639
|
-
if (stderr) this.log?.(`k6 STDERR:\n${stderr}`);
|
|
640
|
-
|
|
641
|
-
if (code !== 0) {
|
|
642
|
-
throw new Error(
|
|
643
|
-
`k6 process exited with code ${code}. Check k6 output for details.`
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
this.log?.(
|
|
647
|
-
`โ
k6 script executed successfully for ${expectedMethod} request.`
|
|
648
|
-
);
|
|
693
|
+
await runK6ScriptFromWorld(this, method);
|
|
694
|
+
}
|
|
695
|
+
);
|
|
649
696
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (!saveK6Script) {
|
|
656
|
-
try {
|
|
657
|
-
fs.unlinkSync(scriptPath);
|
|
658
|
-
this.log?.(`๐งน Temporary k6 script deleted: "${scriptPath}"`);
|
|
659
|
-
} catch (cleanupErr) {
|
|
660
|
-
this.log?.(
|
|
661
|
-
`โ ๏ธ Warning: Could not delete temporary k6 script file: "${scriptPath}". Error: ${
|
|
662
|
-
cleanupErr instanceof Error
|
|
663
|
-
? cleanupErr.message
|
|
664
|
-
: String(cleanupErr)
|
|
665
|
-
}`
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
} else {
|
|
669
|
-
this.log?.(
|
|
670
|
-
`โน๏ธ k6 script kept at: "${scriptPath}". Set SAVE_K6_SCRIPT=false to delete automatically.`
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
} catch (error) {
|
|
674
|
-
this.log?.(
|
|
675
|
-
`โ Failed to generate or run k6 script: ${
|
|
676
|
-
error instanceof Error ? error.message : String(error)
|
|
677
|
-
}`
|
|
678
|
-
);
|
|
679
|
-
throw new Error(
|
|
680
|
-
`k6 script generation or execution failed: ${
|
|
681
|
-
error instanceof Error ? error.message : String(error)
|
|
682
|
-
}`
|
|
683
|
-
);
|
|
684
|
-
}
|
|
697
|
+
// Multi-endpoint
|
|
698
|
+
Then(
|
|
699
|
+
"I run the script for multiple endpoints with method {word}",
|
|
700
|
+
async function (method) {
|
|
701
|
+
await runK6ScriptFromWorld(this, method, true);
|
|
685
702
|
}
|
|
686
703
|
);
|