@testrigor/testrigor-cli 0.0.44-beta → 0.0.46-beta
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/README.md +13 -12
- package/dist/commands/test-suite/run.d.ts +4 -1
- package/dist/commands/test-suite/run.js +108 -13
- package/dist/types.d.ts +51 -0
- package/dist/util/certificate-uploader.d.ts +2 -0
- package/dist/util/certificate-uploader.js +32 -0
- package/dist/util/type-guards.d.ts +4 -0
- package/dist/util/type-guards.js +15 -0
- package/dist/util/variable-file-uploader.d.ts +1 -0
- package/dist/util/variable-file-uploader.js +43 -0
- package/dist/util/variables-path-parser.d.ts +2 -0
- package/dist/util/variables-path-parser.js +126 -0
- package/oclif.manifest.json +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,18 +19,19 @@ This is a command line tool for TestRigor. It allows you to run TestRigor tests
|
|
|
19
19
|
8. With rules `testrigor test-suite run <test-suite-id> --token <token> --rules-path rules/**/*.yaml`
|
|
20
20
|
9. With settings `testrigor test-suite run <test-suite-id> --token <token> --settings-path settings.yaml` (Can be YAML or JSON)
|
|
21
21
|
10. Run a single test case `testrigor test-suite run <test-suite-id> --token <token> --test-case-uuid <test-case-uuid>`
|
|
22
|
-
11. Run
|
|
23
|
-
12. Run with
|
|
24
|
-
13.
|
|
25
|
-
14.
|
|
26
|
-
15.
|
|
27
|
-
16.
|
|
28
|
-
17.
|
|
29
|
-
18.
|
|
30
|
-
19.
|
|
31
|
-
20.
|
|
32
|
-
21.
|
|
33
|
-
22.
|
|
22
|
+
11. Run multiple test cases `testrigor test-suite run <test-suite-id> --token <token> --test-case-uuid <test-case-uuid-1> --test-case-uuid <test-case-uuid-2>`
|
|
23
|
+
12. Run with auto create AI rules enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --auto-create-ai-rules`
|
|
24
|
+
13. Run with explicit mutations enabled `testrigor test-suite run <test-suite-id> --token <token> --test-cases-path test_cases/**/*.{yaml,txt} --explicit-mutations`
|
|
25
|
+
14. With all options `testrigor test-suite run <test-suite-id> --token <token> --url <url> --branch <branch> --commit <commit> --variables-path file.json --localhost --test-cases-path test_cases/**/*.{yaml,txt} --rules-path rules/**/*.{yaml,txt} --auto-create-ai-rules --explicit-mutations`
|
|
26
|
+
15. Save JUnit report to `./junit-report.xml` (works only in sync mode): `testrigor test-suite run <test-suite-id> --token <token> --junit-report-save-path ./report.xml`
|
|
27
|
+
16. With comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --labels <labels>`
|
|
28
|
+
17. Excluding comma-separated labels `testrigor test-suite run <test-suite-id> --token <token> --excluded-labels <excluded-labels>`
|
|
29
|
+
18. With different WEB and API URLs `testrigor test-suite run <test-suite-id> --token <token> --base-url-web=<base-web-url> --base-url-api=<base_api_url>`
|
|
30
|
+
19. Set the default test suite ID: `testrigor test-suite config --default <test-suite-id>`
|
|
31
|
+
20. Remove the default test suite ID: `testrigor test-suite config --delete`
|
|
32
|
+
21. Show current configuration: `testrigor test-suite config --show`
|
|
33
|
+
22. Authenticate with TestRigor and store credentials (interactive prompt): `testrigor authenticate`
|
|
34
|
+
23. Run with override chrome extensions at runtime (one or more chrome extensions): `test-suite run <test-suite-id> --token <token> --chrome-extension "name=<extension-name> file=<extension-file>" --chrome-extension "name=<extension-name> file=<extension-file>"`
|
|
34
35
|
|
|
35
36
|
### Mobile Testing
|
|
36
37
|
|
|
@@ -18,7 +18,7 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
18
18
|
"rules-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
19
19
|
"settings-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
20
20
|
"local-test-nodes-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
21
|
-
"test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
21
|
+
"test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
22
22
|
"auto-create-ai-rules": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
23
23
|
"explicit-mutations": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
24
24
|
labels: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
@@ -36,8 +36,11 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
36
36
|
};
|
|
37
37
|
private initializeUrls;
|
|
38
38
|
run(): Promise<void>;
|
|
39
|
+
private isSingleTestCaseExecution;
|
|
39
40
|
private fetchStatus;
|
|
40
41
|
private getStatusApiEndpoint;
|
|
42
|
+
private uploadVariableFiles;
|
|
43
|
+
private uploadClientCertificates;
|
|
41
44
|
private callRetest;
|
|
42
45
|
private fetchSuiteType;
|
|
43
46
|
private isMobileSuiteType;
|
|
@@ -11,6 +11,9 @@ const chrome_extension_1 = require("../../flags/chrome-extension");
|
|
|
11
11
|
const parser_1 = require("../../util/parser");
|
|
12
12
|
const request_builder_1 = require("../../util/request-builder");
|
|
13
13
|
const tunnel_1 = require("../../util/tunnel");
|
|
14
|
+
const certificate_uploader_1 = require("../../util/certificate-uploader");
|
|
15
|
+
const variable_file_uploader_1 = require("../../util/variable-file-uploader");
|
|
16
|
+
const variables_path_parser_1 = require("../../util/variables-path-parser");
|
|
14
17
|
const appium_1 = require("../../util/appium");
|
|
15
18
|
const utils_1 = require("../../util/utils");
|
|
16
19
|
const sleep = async (milliseconds) => {
|
|
@@ -76,7 +79,7 @@ class Run extends command_base_1.CommandBase {
|
|
|
76
79
|
const testCasesPath = flags["test-cases-path"];
|
|
77
80
|
const rulesPath = flags["rules-path"];
|
|
78
81
|
const settingsPath = flags["settings-path"];
|
|
79
|
-
const
|
|
82
|
+
const testCaseUuids = flags["test-case-uuid"];
|
|
80
83
|
const isAutoCreateAIBasedRulesEnabled = flags["auto-create-ai-rules"];
|
|
81
84
|
const explicitMutations = flags["explicit-mutations"];
|
|
82
85
|
const labels = flags["labels"];
|
|
@@ -105,8 +108,8 @@ class Run extends command_base_1.CommandBase {
|
|
|
105
108
|
if (minPort > maxPort) {
|
|
106
109
|
core_1.ux.error("--minPort cannot be greater than --maxPort");
|
|
107
110
|
}
|
|
108
|
-
if (
|
|
109
|
-
context.configuration.testCaseUuids =
|
|
111
|
+
if (testCaseUuids && testCaseUuids.length > 0) {
|
|
112
|
+
context.configuration.testCaseUuids = testCaseUuids;
|
|
110
113
|
}
|
|
111
114
|
if (isAutoCreateAIBasedRulesEnabled) {
|
|
112
115
|
context.configuration.isAutoCreateAIBasedRulesEnabled =
|
|
@@ -118,10 +121,23 @@ class Run extends command_base_1.CommandBase {
|
|
|
118
121
|
if (variablesPath) {
|
|
119
122
|
try {
|
|
120
123
|
const file = await Fs.content(variablesPath);
|
|
121
|
-
|
|
124
|
+
const parsed = JSON.parse(file.toString());
|
|
125
|
+
const { textValues, fileInputs, certInputs } = (0, variables_path_parser_1.parseVariablesJson)(parsed);
|
|
126
|
+
context.configuration.storedValues = textValues;
|
|
127
|
+
if (fileInputs.length > 0) {
|
|
128
|
+
context.storedValueFileInputs = fileInputs;
|
|
129
|
+
}
|
|
130
|
+
if (certInputs.length > 0) {
|
|
131
|
+
context.storedValueCertInputs = certInputs;
|
|
132
|
+
}
|
|
122
133
|
}
|
|
123
134
|
catch (e) {
|
|
124
|
-
|
|
135
|
+
if (e instanceof SyntaxError) {
|
|
136
|
+
core_1.ux.error("--variables-path must be a valid JSON path");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
core_1.ux.error(e instanceof Error ? e.message : String(e));
|
|
140
|
+
}
|
|
125
141
|
}
|
|
126
142
|
}
|
|
127
143
|
if (testCasesPath) {
|
|
@@ -159,7 +175,7 @@ class Run extends command_base_1.CommandBase {
|
|
|
159
175
|
}
|
|
160
176
|
if (localTestNodesPath) {
|
|
161
177
|
try {
|
|
162
|
-
const localTestNodes = await (0, parser_1.parseSettings)(localTestNodesPath);
|
|
178
|
+
const localTestNodes = (await (0, parser_1.parseSettings)(localTestNodesPath));
|
|
163
179
|
if (localTestNodes) {
|
|
164
180
|
if (Array.isArray(localTestNodes.testNodes)) {
|
|
165
181
|
for (const node of localTestNodes.testNodes) {
|
|
@@ -285,14 +301,26 @@ class Run extends command_base_1.CommandBase {
|
|
|
285
301
|
// Checking last run id
|
|
286
302
|
task: async (ctx) => {
|
|
287
303
|
let pollBranch = flags.branch;
|
|
288
|
-
if (
|
|
289
|
-
pollBranch = `single-test-execution-${
|
|
304
|
+
if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
|
|
305
|
+
pollBranch = `single-test-execution-${testCaseUuids[0]}`;
|
|
290
306
|
}
|
|
291
307
|
const statusResponse = await this.fetchStatus(testSuiteId, token, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
292
308
|
context.lastRunId = (0, utils_1.extractRunIdFromDetailsUrl)(statusResponse.detailsUrl);
|
|
293
309
|
},
|
|
294
310
|
exitOnError: false,
|
|
295
311
|
},
|
|
312
|
+
{
|
|
313
|
+
title: "Uploading variable files",
|
|
314
|
+
enabled: () => !!context.storedValueFileInputs?.length,
|
|
315
|
+
task: (ctx) => this.uploadVariableFiles(ctx, testSuiteId, token),
|
|
316
|
+
exitOnError: false,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
title: "Uploading client certificates",
|
|
320
|
+
enabled: () => !!context.storedValueCertInputs?.length,
|
|
321
|
+
task: (ctx, task) => this.uploadClientCertificates(ctx, testSuiteId, token, task),
|
|
322
|
+
exitOnError: false,
|
|
323
|
+
},
|
|
296
324
|
{
|
|
297
325
|
enabled: () => !flags["file-path"] && !flags["chrome-extension"],
|
|
298
326
|
task: async (ctx) => {
|
|
@@ -451,8 +479,8 @@ class Run extends command_base_1.CommandBase {
|
|
|
451
479
|
enabled: (ctx) => ctx.response?.queueId,
|
|
452
480
|
task: async (ctx, currentTask) => {
|
|
453
481
|
let pollBranch = flags.branch;
|
|
454
|
-
if (
|
|
455
|
-
pollBranch = `single-test-execution-${
|
|
482
|
+
if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
|
|
483
|
+
pollBranch = `single-test-execution-${testCaseUuids[0]}`;
|
|
456
484
|
}
|
|
457
485
|
const detailsUrl = await this.waitForTestSuiteToBeCreated(testSuiteId, token, ctx.lastRunId, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
458
486
|
task.title = `Running test suite [ ${detailsUrl} ]`;
|
|
@@ -465,8 +493,8 @@ class Run extends command_base_1.CommandBase {
|
|
|
465
493
|
enabled: (ctx) => ctx.response?.result === "ok" && !flags.async,
|
|
466
494
|
task: async (ctx, task) => {
|
|
467
495
|
let pollBranch = flags.branch;
|
|
468
|
-
if (
|
|
469
|
-
pollBranch = `single-test-execution-${
|
|
496
|
+
if (this.isSingleTestCaseExecution(testCaseUuids, flags.branch)) {
|
|
497
|
+
pollBranch = `single-test-execution-${testCaseUuids[0]}`;
|
|
470
498
|
}
|
|
471
499
|
return task.newListr([
|
|
472
500
|
{
|
|
@@ -535,6 +563,9 @@ class Run extends command_base_1.CommandBase {
|
|
|
535
563
|
}
|
|
536
564
|
this.exit(0);
|
|
537
565
|
}
|
|
566
|
+
isSingleTestCaseExecution(testCaseUuids, branch) {
|
|
567
|
+
return !!testCaseUuids && testCaseUuids.length === 1 && !branch;
|
|
568
|
+
}
|
|
538
569
|
async fetchStatus(testSuiteId, token, branch, labels, excludedLabels) {
|
|
539
570
|
const statusApiEndpoint = this.getStatusApiEndpoint(testSuiteId, branch, labels, excludedLabels);
|
|
540
571
|
const response = await (0, node_fetch_commonjs_1.default)(statusApiEndpoint, {
|
|
@@ -564,6 +595,69 @@ class Run extends command_base_1.CommandBase {
|
|
|
564
595
|
const queryString = queryParams.toString();
|
|
565
596
|
return queryString ? `${apiEndpoint}?${queryString}` : apiEndpoint;
|
|
566
597
|
}
|
|
598
|
+
async uploadVariableFiles(ctx, testSuiteId, token) {
|
|
599
|
+
const controller = new AbortController();
|
|
600
|
+
const timeoutId = setTimeout(() => controller.abort(), utils_1.UPLOAD_TIMEOUT_MS);
|
|
601
|
+
try {
|
|
602
|
+
ctx.configuration.storedValues = ctx.configuration.storedValues || {};
|
|
603
|
+
const uploads = await Promise.all(ctx.storedValueFileInputs.map(async (input) => ({
|
|
604
|
+
name: input.name,
|
|
605
|
+
url: await (0, variable_file_uploader_1.uploadVariableFile)(this.TESTRIGOR_API2_URL, testSuiteId, token, input.file),
|
|
606
|
+
})));
|
|
607
|
+
for (const { name, url } of uploads) {
|
|
608
|
+
ctx.configuration.storedValues[name] = url;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (error.name === "AbortError") {
|
|
613
|
+
throw new Error("Variable files upload timed out. Please check your network connection.");
|
|
614
|
+
}
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
clearTimeout(timeoutId);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async uploadClientCertificates(ctx, testSuiteId, token, task) {
|
|
622
|
+
const controller = new AbortController();
|
|
623
|
+
const timeoutId = setTimeout(() => controller.abort(), utils_1.UPLOAD_TIMEOUT_MS);
|
|
624
|
+
try {
|
|
625
|
+
ctx.configuration.clientCertificateOverrides =
|
|
626
|
+
ctx.configuration.clientCertificateOverrides || {};
|
|
627
|
+
const uploads = await Promise.all(ctx.storedValueCertInputs.map(async (input) => {
|
|
628
|
+
const uploaded = await (0, certificate_uploader_1.uploadClientCertificate)(this.TESTRIGOR_API2_URL, testSuiteId, token, input.file, input.password);
|
|
629
|
+
return {
|
|
630
|
+
name: input.name,
|
|
631
|
+
certificateId: uploaded.certificateId,
|
|
632
|
+
info: uploaded.info,
|
|
633
|
+
};
|
|
634
|
+
}));
|
|
635
|
+
for (const { name, certificateId } of uploads) {
|
|
636
|
+
ctx.configuration.clientCertificateOverrides[name] = certificateId;
|
|
637
|
+
}
|
|
638
|
+
const summary = uploads
|
|
639
|
+
.map(({ name, info }) => {
|
|
640
|
+
const subject = info?.subject?.commonName || info?.subject?.distinguishedName;
|
|
641
|
+
const notAfter = info?.notAfter
|
|
642
|
+
? ` (expires ${info.notAfter.slice(0, 10)})`
|
|
643
|
+
: "";
|
|
644
|
+
return subject ? `${name}: ${subject}${notAfter}` : name;
|
|
645
|
+
})
|
|
646
|
+
.join("; ");
|
|
647
|
+
if (summary) {
|
|
648
|
+
task.title = `Uploaded client certificate(s): ${summary}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
if (error.name === "AbortError") {
|
|
653
|
+
throw new Error("Client certificates upload timed out. Please check your network connection.");
|
|
654
|
+
}
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
clearTimeout(timeoutId);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
567
661
|
async callRetest(testSuiteId, configuration, token) {
|
|
568
662
|
return (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API_URL}/apps/${testSuiteId}/retest`, {
|
|
569
663
|
method: "POST",
|
|
@@ -794,7 +888,8 @@ Run.flags = {
|
|
|
794
888
|
description: "Path to a YAML or JSON file with local test nodes configuration",
|
|
795
889
|
}),
|
|
796
890
|
"test-case-uuid": core_1.Flags.string({
|
|
797
|
-
description: "Test case uuid",
|
|
891
|
+
description: "Test case uuid (can be repeated to run multiple test cases)",
|
|
892
|
+
multiple: true,
|
|
798
893
|
}),
|
|
799
894
|
"auto-create-ai-rules": core_1.Flags.boolean({
|
|
800
895
|
description: "Enable auto create AI based rules",
|
package/dist/types.d.ts
CHANGED
|
@@ -65,3 +65,54 @@ export type ChromeExtensionOverrideItem = {
|
|
|
65
65
|
export type ChromeExtensionOverridesRequest = {
|
|
66
66
|
chromeExtensions: ChromeExtensionOverrideItem[];
|
|
67
67
|
};
|
|
68
|
+
export type StoredValueFileInput = {
|
|
69
|
+
name: string;
|
|
70
|
+
file: string;
|
|
71
|
+
};
|
|
72
|
+
export type StoredValueCertInput = {
|
|
73
|
+
name: string;
|
|
74
|
+
file: string;
|
|
75
|
+
password: string;
|
|
76
|
+
};
|
|
77
|
+
export type ParsedVariables = {
|
|
78
|
+
textValues: Record<string, string>;
|
|
79
|
+
fileInputs: StoredValueFileInput[];
|
|
80
|
+
certInputs: StoredValueCertInput[];
|
|
81
|
+
};
|
|
82
|
+
export type ClientCertificateDistinguishedName = {
|
|
83
|
+
distinguishedName?: string;
|
|
84
|
+
commonName?: string;
|
|
85
|
+
};
|
|
86
|
+
export type ClientCertificateInfo = {
|
|
87
|
+
serialNumber?: string;
|
|
88
|
+
notBefore?: string;
|
|
89
|
+
notAfter?: string;
|
|
90
|
+
issuer?: ClientCertificateDistinguishedName;
|
|
91
|
+
subject?: ClientCertificateDistinguishedName;
|
|
92
|
+
};
|
|
93
|
+
export type CertificateUploadDataApi2Response = {
|
|
94
|
+
certificateId: string;
|
|
95
|
+
info?: ClientCertificateInfo;
|
|
96
|
+
};
|
|
97
|
+
export type UploadedClientCertificate = {
|
|
98
|
+
certificateId: string;
|
|
99
|
+
info?: ClientCertificateInfo;
|
|
100
|
+
};
|
|
101
|
+
export type CertificateUploadApi2Response = Api2Wrapper & {
|
|
102
|
+
data: CertificateUploadDataApi2Response;
|
|
103
|
+
};
|
|
104
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
105
|
+
[key: string]: JsonValue;
|
|
106
|
+
};
|
|
107
|
+
export type JsonObject = {
|
|
108
|
+
[key: string]: JsonValue;
|
|
109
|
+
};
|
|
110
|
+
export type FileVariableObject = {
|
|
111
|
+
type: "FILE";
|
|
112
|
+
value: string;
|
|
113
|
+
};
|
|
114
|
+
export type CertVariableObject = {
|
|
115
|
+
type: "CLIENT_CERTIFICATE";
|
|
116
|
+
value: string;
|
|
117
|
+
password: string;
|
|
118
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadClientCertificate = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const node_fetch_commonjs_1 = require("node-fetch-commonjs");
|
|
7
|
+
const type_guards_1 = require("./type-guards");
|
|
8
|
+
async function uploadClientCertificate(api2BaseUrl, testSuiteId, token, localFilePath, password) {
|
|
9
|
+
const fileName = path.basename(localFilePath);
|
|
10
|
+
const certBase64 = fs.readFileSync(localFilePath).toString("base64");
|
|
11
|
+
const response = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/client_certificate`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
body: JSON.stringify({ fileName, certBase64, password }),
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
"auth-token": token,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const responseJson = await response.json();
|
|
21
|
+
if (!(0, type_guards_1.isCertificateUploadApi2Response)(responseJson)) {
|
|
22
|
+
throw new Error(`Unexpected response shape from client certificate upload endpoint for '${fileName}'`);
|
|
23
|
+
}
|
|
24
|
+
if (!response.ok || responseJson.status !== 200 || !responseJson.data) {
|
|
25
|
+
throw new Error(`Failed to upload client certificate '${fileName}': ${responseJson.message}`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
certificateId: responseJson.data.certificateId,
|
|
29
|
+
info: responseJson.data.info,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
exports.uploadClientCertificate = uploadClientCertificate;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CertificateUploadApi2Response, SignedUrlApi2Response } from "../types";
|
|
2
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function isSignedUrlApi2Response(value: unknown): value is SignedUrlApi2Response;
|
|
4
|
+
export declare function isCertificateUploadApi2Response(value: unknown): value is CertificateUploadApi2Response;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCertificateUploadApi2Response = exports.isSignedUrlApi2Response = exports.isPlainObject = void 0;
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
exports.isPlainObject = isPlainObject;
|
|
8
|
+
function isSignedUrlApi2Response(value) {
|
|
9
|
+
return isPlainObject(value) && "status" in value;
|
|
10
|
+
}
|
|
11
|
+
exports.isSignedUrlApi2Response = isSignedUrlApi2Response;
|
|
12
|
+
function isCertificateUploadApi2Response(value) {
|
|
13
|
+
return isPlainObject(value) && "status" in value;
|
|
14
|
+
}
|
|
15
|
+
exports.isCertificateUploadApi2Response = isCertificateUploadApi2Response;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function uploadVariableFile(api2BaseUrl: string, testSuiteId: string, token: string, localFilePath: string): Promise<string>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uploadVariableFile = void 0;
|
|
4
|
+
const Fs = require("@supercharge/fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const node_fetch_commonjs_1 = require("node-fetch-commonjs");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const type_guards_1 = require("./type-guards");
|
|
9
|
+
async function uploadVariableFile(api2BaseUrl, testSuiteId, token, localFilePath) {
|
|
10
|
+
const fileName = path.basename(localFilePath);
|
|
11
|
+
const fileSize = Fs.statSync(localFilePath).size;
|
|
12
|
+
const presignedResp = await (0, node_fetch_commonjs_1.default)(`${api2BaseUrl}/apps/${testSuiteId}/test_data/global_variables/file/pre_signed_url`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
body: JSON.stringify({ fileName }),
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
Accept: "application/json",
|
|
18
|
+
"auth-token": token,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const presignedJson = await presignedResp.json();
|
|
22
|
+
if (!(0, type_guards_1.isSignedUrlApi2Response)(presignedJson)) {
|
|
23
|
+
throw new Error(`Unexpected response shape from presigned URL endpoint for '${fileName}'`);
|
|
24
|
+
}
|
|
25
|
+
if (!presignedResp.ok ||
|
|
26
|
+
presignedJson.status !== 200 ||
|
|
27
|
+
!presignedJson.data) {
|
|
28
|
+
throw new Error(`Failed to request presigned URL for variable file '${fileName}': ${presignedJson.message}`);
|
|
29
|
+
}
|
|
30
|
+
const fileStream = Fs.createReadStream(localFilePath);
|
|
31
|
+
const putResp = await (0, node_fetch_commonjs_1.default)(presignedJson.data.url, {
|
|
32
|
+
method: "PUT",
|
|
33
|
+
body: fileStream,
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-length": `${fileSize}`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!putResp.ok) {
|
|
39
|
+
throw new Error(`Failed to upload variable file '${fileName}' to S3: ${putResp.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
return (0, utils_1.extractUrlWithoutQueryParams)(presignedJson.data.url);
|
|
42
|
+
}
|
|
43
|
+
exports.uploadVariableFile = uploadVariableFile;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseVariablesJson = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const type_guards_1 = require("./type-guards");
|
|
7
|
+
const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
|
|
8
|
+
const MAX_COMBINED_SIZE_BYTES = 100 * 1024 * 1024;
|
|
9
|
+
const MAX_SIZE_LABEL = "100 MB";
|
|
10
|
+
const MAX_CERT_FILE_SIZE_BYTES = 20 * 1024; // 20 KB
|
|
11
|
+
const MAX_CERT_PASSWORD_LENGTH = 100;
|
|
12
|
+
const MAX_CERT_FILENAME_LENGTH = 100;
|
|
13
|
+
const ERROR_ROOT_NOT_OBJECT = "--variables-path must contain a JSON object at the top level";
|
|
14
|
+
const ERROR_COMBINED_SIZE_EXCEEDED = `Variable files combined size exceeds the maximum allowed (${MAX_SIZE_LABEL})`;
|
|
15
|
+
function isUrl(value) {
|
|
16
|
+
return /^https?:\/\//i.test(value);
|
|
17
|
+
}
|
|
18
|
+
function isFileVariableObject(value) {
|
|
19
|
+
return ((0, type_guards_1.isPlainObject)(value) &&
|
|
20
|
+
value.type === "FILE" &&
|
|
21
|
+
typeof value.value === "string");
|
|
22
|
+
}
|
|
23
|
+
function isCertVariableObject(value) {
|
|
24
|
+
return ((0, type_guards_1.isPlainObject)(value) &&
|
|
25
|
+
value.type === "CLIENT_CERTIFICATE" &&
|
|
26
|
+
typeof value.value === "string" &&
|
|
27
|
+
typeof value.password === "string");
|
|
28
|
+
}
|
|
29
|
+
function readLocalFileSize(key, filePath) {
|
|
30
|
+
if (!fs.existsSync(filePath)) {
|
|
31
|
+
throw new Error(`Variable "${key}": file not found at path "${filePath}"`);
|
|
32
|
+
}
|
|
33
|
+
const size = fs.statSync(filePath).size;
|
|
34
|
+
if (size > MAX_FILE_SIZE_BYTES) {
|
|
35
|
+
throw new Error(`Variable "${key}": file size exceeds the maximum allowed (${MAX_SIZE_LABEL})`);
|
|
36
|
+
}
|
|
37
|
+
return size;
|
|
38
|
+
}
|
|
39
|
+
function validateLocalCertFile(key, value) {
|
|
40
|
+
if (isUrl(value)) {
|
|
41
|
+
throw new Error(`Variable "${key}": CLIENT_CERTIFICATE does not support URL values; provide a local .pfx file path`);
|
|
42
|
+
}
|
|
43
|
+
if (!fs.existsSync(value)) {
|
|
44
|
+
throw new Error(`Variable "${key}": file not found at path "${value}"`);
|
|
45
|
+
}
|
|
46
|
+
if (!/\.pfx$/i.test(path.basename(value))) {
|
|
47
|
+
throw new Error(`Variable "${key}": CLIENT_CERTIFICATE requires a .pfx file`);
|
|
48
|
+
}
|
|
49
|
+
if (path.basename(value).length > MAX_CERT_FILENAME_LENGTH) {
|
|
50
|
+
throw new Error(`Variable "${key}": filename is too long (max ${MAX_CERT_FILENAME_LENGTH} chars)`);
|
|
51
|
+
}
|
|
52
|
+
if (fs.statSync(value).size > MAX_CERT_FILE_SIZE_BYTES) {
|
|
53
|
+
throw new Error(`Variable "${key}": certificate file exceeds the maximum allowed (20 KB)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseVariablesJson(parsed) {
|
|
57
|
+
if (!(0, type_guards_1.isPlainObject)(parsed)) {
|
|
58
|
+
throw new Error(ERROR_ROOT_NOT_OBJECT);
|
|
59
|
+
}
|
|
60
|
+
const textValues = {};
|
|
61
|
+
const fileInputs = [];
|
|
62
|
+
const certInputs = [];
|
|
63
|
+
let combinedSize = 0;
|
|
64
|
+
for (const [key, raw] of Object.entries(parsed)) {
|
|
65
|
+
if (typeof raw === "string") {
|
|
66
|
+
textValues[key] = raw;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!(0, type_guards_1.isPlainObject)(raw)) {
|
|
70
|
+
throw new Error(`Variable "${key}": expected a string or { type: "FILE", value: "..." }`);
|
|
71
|
+
}
|
|
72
|
+
if (raw.type === "FILE" && raw.password !== undefined) {
|
|
73
|
+
throw new Error(`Variable "${key}": "password" field is only valid for CLIENT_CERTIFICATE. Did you mean type:"CLIENT_CERTIFICATE"?`);
|
|
74
|
+
}
|
|
75
|
+
if (raw.type === "FILE") {
|
|
76
|
+
if (!isFileVariableObject(raw)) {
|
|
77
|
+
throw new Error(`Variable "${key}": missing or empty "value" for type:FILE`);
|
|
78
|
+
}
|
|
79
|
+
const value = raw.value.trim();
|
|
80
|
+
if (value.length === 0) {
|
|
81
|
+
throw new Error(`Variable "${key}": missing or empty "value" for type:FILE`);
|
|
82
|
+
}
|
|
83
|
+
if (isUrl(value)) {
|
|
84
|
+
textValues[key] = value;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
combinedSize += readLocalFileSize(key, value);
|
|
88
|
+
if (combinedSize > MAX_COMBINED_SIZE_BYTES) {
|
|
89
|
+
throw new Error(ERROR_COMBINED_SIZE_EXCEEDED);
|
|
90
|
+
}
|
|
91
|
+
fileInputs.push({ name: key, file: value });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (raw.type === "CLIENT_CERTIFICATE") {
|
|
95
|
+
if (!isCertVariableObject(raw)) {
|
|
96
|
+
if (typeof raw.value !== "string" || raw.value.trim().length === 0) {
|
|
97
|
+
throw new Error(`Variable "${key}": missing or empty "value" for type:CLIENT_CERTIFICATE`);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Variable "${key}": missing or empty "password" for type:CLIENT_CERTIFICATE`);
|
|
100
|
+
}
|
|
101
|
+
const value = raw.value.trim();
|
|
102
|
+
if (value.length === 0) {
|
|
103
|
+
throw new Error(`Variable "${key}": missing or empty "value" for type:CLIENT_CERTIFICATE`);
|
|
104
|
+
}
|
|
105
|
+
if (raw.password.length === 0) {
|
|
106
|
+
throw new Error(`Variable "${key}": missing or empty "password" for type:CLIENT_CERTIFICATE`);
|
|
107
|
+
}
|
|
108
|
+
if (raw.password.length > MAX_CERT_PASSWORD_LENGTH) {
|
|
109
|
+
throw new Error(`Variable "${key}": "password" exceeds the maximum length of ${MAX_CERT_PASSWORD_LENGTH} characters`);
|
|
110
|
+
}
|
|
111
|
+
validateLocalCertFile(key, value);
|
|
112
|
+
certInputs.push({
|
|
113
|
+
name: key,
|
|
114
|
+
file: value,
|
|
115
|
+
password: raw.password,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (typeof raw.type === "string" && raw.type.toUpperCase() === "FILE") {
|
|
120
|
+
throw new Error(`Variable "${key}": unsupported type "${String(raw.type)}". Only "FILE" (uppercase) and "CLIENT_CERTIFICATE" are supported.`);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Variable "${key}": unsupported type "${String(raw.type)}". Only "FILE" and "CLIENT_CERTIFICATE" are supported.`);
|
|
123
|
+
}
|
|
124
|
+
return { textValues, fileInputs, certInputs };
|
|
125
|
+
}
|
|
126
|
+
exports.parseVariablesJson = parseVariablesJson;
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.46-beta",
|
|
3
3
|
"commands": {
|
|
4
4
|
"authenticate": {
|
|
5
5
|
"id": "authenticate",
|
|
@@ -204,8 +204,8 @@
|
|
|
204
204
|
"test-case-uuid": {
|
|
205
205
|
"name": "test-case-uuid",
|
|
206
206
|
"type": "option",
|
|
207
|
-
"description": "Test case uuid",
|
|
208
|
-
"multiple":
|
|
207
|
+
"description": "Test case uuid (can be repeated to run multiple test cases)",
|
|
208
|
+
"multiple": true
|
|
209
209
|
},
|
|
210
210
|
"auto-create-ai-rules": {
|
|
211
211
|
"name": "auto-create-ai-rules",
|